feat: 国际化 / 键盘操作 / 无障碍 / 性能优化
国际化 - 新增 ui/i18n 双语文案表(中文原文为 key,英文集中映射,tf 支持占位符) - AppSettings 增加 language(跟随系统/中文/English),启动与保存时即时生效 - 全部界面文案(标题栏/侧栏/工作区/发布/订阅/表单/设置/全部对话框/通知)入表 键盘操作 - Ctrl+W 关闭当前标签页;Ctrl+Tab / Ctrl+Shift+Tab 循环切换标签 - Ctrl+F 聚焦当前工作区消息过滤框 无障碍 - 全部纯图标按钮补 accessibility_label(屏幕阅读器 / UIA 自动化可识别) 性能 - 折叠态预览(JSON 美化/压平+截断)改为入环形缓冲时计算一次, 随 MqttRecord 缓存(Arc<str>),渲染热路径对最多数百条可见行 不再逐帧重复 serde_json 解析
This commit is contained in:
+102
-100
@@ -15,6 +15,7 @@ use gpui_kit::component::{
|
||||
};
|
||||
use gpui_kit::prelude::FluentBuilder as _;
|
||||
use crate::ui::IconName;
|
||||
use crate::ui::i18n;
|
||||
use gpui_kit::{
|
||||
div, percentage, px, App, AppContext as _, Context, Entity, InteractiveElement as _,
|
||||
IntoElement, ParentElement as _, Render, SharedString, StatefulInteractiveElement as _,
|
||||
@@ -25,11 +26,11 @@ use crate::model::{ConnectionConfig, LastWill, ProtocolVersion, SslConfig, Trans
|
||||
use crate::mqtt::MqttEngine;
|
||||
use crate::ui::widgets::{field, field_ex, make_select, OptionDelegate};
|
||||
|
||||
const PROTOCOLS: [&str; 2] = ["MQTT 5.0", "MQTT 3.1.1"];
|
||||
const TRANSPORTS: [&str; 4] = ["TCP", "TLS", "WebSocket", "WSS"];
|
||||
const QOS: [&str; 3] = ["QoS 0", "QoS 1", "QoS 2"];
|
||||
fn protocol_labels() -> [&'static str; 2] { [i18n::t("MQTT 5.0"), "MQTT 3.1.1"] }
|
||||
const TRANSPORTS: [&str; 4] = ["TCP", "TLS", "WebSocket", "WSS"]; // 语言无关
|
||||
const QOS: [&str; 3] = ["QoS 0", "QoS 1", "QoS 2"]; // 语言无关
|
||||
/// 分组下拉的首项:选择它表示不归属任何分组。
|
||||
const NO_GROUP: &str = "无分组";
|
||||
fn no_group_label() -> &'static str { i18n::t("无分组") }
|
||||
|
||||
pub struct ConnectionForm {
|
||||
engine: Arc<MqttEngine>,
|
||||
@@ -142,28 +143,28 @@ impl ConnectionForm {
|
||||
chrono::Local::now().timestamp()
|
||||
});
|
||||
|
||||
let name = input(&c.name, "连接名称", window, cx);
|
||||
let host = input(&c.host, "broker.example.com", window, cx);
|
||||
let port = input(&c.port.to_string(), "端口", window, cx);
|
||||
let path = input(&c.path, "/mqtt", window, cx);
|
||||
let client_id = input(&c.client_id, "Client ID", window, cx);
|
||||
let username = input(&c.username, "用户名(可选)", window, cx);
|
||||
let name = input(&c.name, i18n::t("连接名称"), window, cx);
|
||||
let host = input(&c.host, i18n::t("broker.example.com"), window, cx);
|
||||
let port = input(&c.port.to_string(), i18n::t(i18n::t("端口")), window, cx);
|
||||
let path = input(&c.path, i18n::t("/mqtt"), window, cx);
|
||||
let client_id = input(&c.client_id, i18n::t("Client ID"), window, cx);
|
||||
let username = input(&c.username, i18n::t("用户名(可选)"), window, cx);
|
||||
let password = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.placeholder("密码(可选)")
|
||||
.placeholder(i18n::t("密码(可选)"))
|
||||
.default_value(&c.password)
|
||||
.masked(true)
|
||||
});
|
||||
let keep_alive = input(&c.keep_alive.to_string(), "60", window, cx);
|
||||
let keep_alive = input(&c.keep_alive.to_string(), i18n::t("60"), window, cx);
|
||||
let conn_timeout = input(
|
||||
&c.connection_timeout_secs.to_string(),
|
||||
"秒(默认 10)",
|
||||
i18n::t("秒(默认 10)"),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let max_reconnect = input(
|
||||
&c.max_reconnect_times.to_string(),
|
||||
"0=无限重连",
|
||||
i18n::t("0=无限重连"),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -173,44 +174,44 @@ impl ConnectionForm {
|
||||
} else {
|
||||
String::new()
|
||||
},
|
||||
"秒(0=会话随连接结束)",
|
||||
i18n::t("秒(0=会话随连接结束)"),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let receive_max = optional_input(c.receive_maximum.map(u32::from), "服务端接收上限", window, cx);
|
||||
let max_packet = optional_input(c.maximum_packet_size, "最大报文长度", window, cx);
|
||||
let topic_alias = optional_input(c.topic_alias_maximum.map(u32::from), "主题别名上限", window, cx);
|
||||
let receive_max = optional_input(c.receive_maximum.map(u32::from), i18n::t("服务端接收上限"), window, cx);
|
||||
let max_packet = optional_input(c.maximum_packet_size, i18n::t("最大报文长度"), window, cx);
|
||||
let topic_alias = optional_input(c.topic_alias_maximum.map(u32::from), i18n::t("主题别名上限"), window, cx);
|
||||
|
||||
let ssl_ca = input(&c.ssl.ca_file, "CA 证书 PEM 路径(可选)", window, cx);
|
||||
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 ssl_ca = input(&c.ssl.ca_file, i18n::t("CA 证书 PEM 路径(可选)"), window, cx);
|
||||
let ssl_client_cert = input(&c.ssl.client_cert_file, i18n::t("客户端证书 PEM 路径(可选)"), window, cx);
|
||||
let ssl_client_key = input(&c.ssl.client_key_file, i18n::t("客户端私钥 PEM 路径(可选)"), window, cx);
|
||||
|
||||
let group_new = input("", "填写后优先于左侧所选分组", window, cx);
|
||||
let group_new = input("", i18n::t("填写后优先于左侧所选分组"), window, cx);
|
||||
|
||||
let (will_topic, will_payload, will_enabled, will_retain, will_qos_idx, will_content_type, will_response_topic) =
|
||||
match &c.last_will {
|
||||
Some(w) => (
|
||||
input(&w.topic, "遗嘱主题", window, cx),
|
||||
input(&w.payload, "遗嘱消息", window, cx),
|
||||
input(&w.topic, i18n::t("遗嘱主题"), window, cx),
|
||||
input(&w.payload, i18n::t("遗嘱消息"), window, cx),
|
||||
true,
|
||||
w.retain,
|
||||
w.qos as usize,
|
||||
input(w.content_type.as_deref().unwrap_or(""), "内容类型(可选)", window, cx),
|
||||
input(w.response_topic.as_deref().unwrap_or(""), "响应主题(可选)", window, cx),
|
||||
input(w.content_type.as_deref().unwrap_or(""), i18n::t("内容类型(可选)"), window, cx),
|
||||
input(w.response_topic.as_deref().unwrap_or(""), i18n::t("响应主题(可选)"), window, cx),
|
||||
),
|
||||
None => (
|
||||
input("", "遗嘱主题", window, cx),
|
||||
input("", "遗嘱消息", window, cx),
|
||||
input("", i18n::t("遗嘱主题"), window, cx),
|
||||
input("", i18n::t("遗嘱消息"), window, cx),
|
||||
false,
|
||||
false,
|
||||
0,
|
||||
input("", "内容类型(可选)", window, cx),
|
||||
input("", "响应主题(可选)", window, cx),
|
||||
input("", i18n::t("内容类型(可选)"), window, cx),
|
||||
input("", i18n::t("响应主题(可选)"), window, cx),
|
||||
),
|
||||
};
|
||||
|
||||
let protocol = make_select(
|
||||
&PROTOCOLS,
|
||||
&protocol_labels(),
|
||||
if c.protocol.is_v5() { 0 } else { 1 },
|
||||
window,
|
||||
cx,
|
||||
@@ -222,7 +223,7 @@ impl ConnectionForm {
|
||||
let transport = make_select(&TRANSPORTS, transport_idx, window, cx);
|
||||
let will_qos = make_select(&QOS, will_qos_idx, window, cx);
|
||||
// 分组下拉:首项固定为「无分组」,其后是已有分组
|
||||
let mut group_labels = vec![NO_GROUP];
|
||||
let mut group_labels = vec![no_group_label()];
|
||||
group_labels.extend(groups.iter().map(|s| s.as_str()));
|
||||
let group_idx = c
|
||||
.group
|
||||
@@ -398,39 +399,39 @@ impl ConnectionForm {
|
||||
r.map_err(|e| self.fail(key, e))
|
||||
}
|
||||
|
||||
fn parse_u16(s: &str, label: &str) -> Result<u16, String> {
|
||||
fn parse_u16(s: &str, label: &'static str) -> Result<u16, String> {
|
||||
s.trim()
|
||||
.parse::<u16>()
|
||||
.map_err(|_| format!("{label} 需要是 0~65535 的整数"))
|
||||
.map_err(|_| format!("{} {}", i18n::t(label), i18n::t("需要是 0~65535 的整数")))
|
||||
}
|
||||
|
||||
/// 端口:1~65535。0 不是可连接端口,放行只会在 CONNECT 阶段报出难以理解的错误。
|
||||
fn parse_port(s: &str) -> Result<u16, String> {
|
||||
match s.trim().parse::<u16>() {
|
||||
Ok(0) => Err("端口需要是 1~65535 的整数".into()),
|
||||
Ok(0) => Err(i18n::t("端口需要是 1~65535 的整数").to_string()),
|
||||
Ok(p) => Ok(p),
|
||||
Err(_) => Err("端口需要是 1~65535 的整数".into()),
|
||||
Err(_) => Err(i18n::t("端口需要是 1~65535 的整数").to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 同 `parse_opt_u32`,但 0 视为非法:MQTT 5 规范把 Receive Maximum /
|
||||
/// Maximum Packet Size 为 0 定义为 Protocol Error,broker 会直接断开连接,
|
||||
/// 且该值会被持久化,表现为"每次连接都失败"。
|
||||
fn parse_opt_positive_u32(s: &str, label: &str) -> Result<Option<u32>, String> {
|
||||
fn parse_opt_positive_u32(s: &str, label: &'static str) -> Result<Option<u32>, String> {
|
||||
match Self::parse_opt_u32(s, label)? {
|
||||
Some(0) => Err(format!("{label} 必须大于 0(0 是 MQTT 5 协议错误)")),
|
||||
Some(0) => Err(format!("{} {}", i18n::t(label), i18n::t("必须大于 0(0 是 MQTT 5 协议错误)"))),
|
||||
other => Ok(other),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_opt_u32(s: &str, label: &str) -> Result<Option<u32>, String> {
|
||||
fn parse_opt_u32(s: &str, label: &'static str) -> Result<Option<u32>, String> {
|
||||
let s = s.trim();
|
||||
if s.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
s.parse::<u32>()
|
||||
.map(Some)
|
||||
.map_err(|_| format!("{label} 需要是非负整数"))
|
||||
.map_err(|_| format!("{} {}", i18n::t(label), i18n::t("需要是非负整数")))
|
||||
}
|
||||
|
||||
pub fn build(&self, cx: &App) -> Result<ConnectionConfig, String> {
|
||||
@@ -450,28 +451,28 @@ impl ConnectionForm {
|
||||
let mut name = self.val(&self.name, cx);
|
||||
if name.trim().is_empty() {
|
||||
if !for_test {
|
||||
return Err(self.fail("name", "连接名称不能为空"));
|
||||
return Err(self.fail("name", i18n::t("连接名称不能为空")));
|
||||
}
|
||||
name = "测试连接".into();
|
||||
name = i18n::t("测试连接").into();
|
||||
}
|
||||
// 判空已按 trim,保存也存 trim 后的值,避免首尾空白入库
|
||||
let name = name.trim().to_string();
|
||||
let host = self.val(&self.host, cx);
|
||||
if host.trim().is_empty() {
|
||||
return Err(self.fail("host", "主机地址不能为空"));
|
||||
return Err(self.fail("host", i18n::t("主机地址不能为空")));
|
||||
}
|
||||
let host = host.trim().to_string();
|
||||
let port = self.fail_as("port", Self::parse_port(&self.val(&self.port, cx)))?;
|
||||
let mut client_id = self.val(&self.client_id, cx);
|
||||
if client_id.trim().is_empty() {
|
||||
if !for_test {
|
||||
return Err(self.fail("client_id", "Client ID 不能为空"));
|
||||
return Err(self.fail("client_id", i18n::t("Client ID 不能为空")));
|
||||
}
|
||||
client_id = format!("mqttx-test-{}", &uuid::Uuid::new_v4().simple().to_string()[..8]);
|
||||
}
|
||||
let client_id = client_id.trim().to_string();
|
||||
let keep_alive =
|
||||
self.fail_as("keep_alive", Self::parse_u16(&self.val(&self.keep_alive, cx), "Keep Alive"))?;
|
||||
self.fail_as("keep_alive", Self::parse_u16(&self.val(&self.keep_alive, cx), i18n::t("Keep Alive")))?;
|
||||
// 空串视为 0(会话随连接结束);非空必须是合法非负整数,不再静默吞掉 -1/abc
|
||||
let session_expiry = {
|
||||
let raw = self.val(&self.session_expiry, cx);
|
||||
@@ -482,23 +483,23 @@ impl ConnectionForm {
|
||||
t.parse::<u32>().map_err(|_| {
|
||||
self.fail(
|
||||
"session_expiry",
|
||||
"会话过期间隔需要是非负整数(0=会话随连接结束)",
|
||||
i18n::t("会话过期间隔需要是非负整数(0=会话随连接结束)"),
|
||||
)
|
||||
})?
|
||||
}
|
||||
};
|
||||
let receive_maximum = self.fail_as("receive_max", (|| -> Result<_, String> {
|
||||
Ok(Self::parse_opt_positive_u32(&self.val(&self.receive_max, cx), "接收上限")?
|
||||
.map(|v| u16::try_from(v).map_err(|_| "接收上限不能超过 65535".to_string()))
|
||||
Ok(Self::parse_opt_positive_u32(&self.val(&self.receive_max, cx), i18n::t("接收上限"))?
|
||||
.map(|v| u16::try_from(v).map_err(|_| i18n::t("接收上限不能超过 65535").to_string()))
|
||||
.transpose())
|
||||
})())??;
|
||||
let maximum_packet_size = self.fail_as(
|
||||
"max_packet",
|
||||
Self::parse_opt_positive_u32(&self.val(&self.max_packet, cx), "最大报文长度"),
|
||||
Self::parse_opt_positive_u32(&self.val(&self.max_packet, cx), i18n::t("最大报文长度")),
|
||||
)?;
|
||||
let topic_alias_maximum = self.fail_as("topic_alias", (|| -> Result<_, String> {
|
||||
Ok(Self::parse_opt_positive_u32(&self.val(&self.topic_alias, cx), "主题别名")?
|
||||
.map(|v| u16::try_from(v).map_err(|_| "主题别名上限不能超过 65535".to_string()))
|
||||
Ok(Self::parse_opt_positive_u32(&self.val(&self.topic_alias, cx), i18n::t("主题别名"))?
|
||||
.map(|v| u16::try_from(v).map_err(|_| i18n::t("主题别名上限不能超过 65535").to_string()))
|
||||
.transpose())
|
||||
})())??;
|
||||
|
||||
@@ -511,7 +512,7 @@ impl ConnectionForm {
|
||||
.parse::<u16>()
|
||||
.ok()
|
||||
.filter(|v| *v >= 1)
|
||||
.ok_or_else(|| self.fail("conn_timeout", "连接超时需要是 1~65535 的整数(秒)"))?
|
||||
.ok_or_else(|| self.fail("conn_timeout", i18n::t("连接超时需要是 1~65535 的整数(秒)")))?
|
||||
};
|
||||
let max_reconnect_raw = self.val(&self.max_reconnect, cx);
|
||||
let max_reconnect_times = if max_reconnect_raw.trim().is_empty() {
|
||||
@@ -520,7 +521,7 @@ impl ConnectionForm {
|
||||
max_reconnect_raw
|
||||
.trim()
|
||||
.parse::<u32>()
|
||||
.map_err(|_| self.fail("max_reconnect", "最大重连次数需要是非负整数(0=无限)"))?
|
||||
.map_err(|_| self.fail("max_reconnect", i18n::t("最大重连次数需要是非负整数(0=无限)")))?
|
||||
};
|
||||
|
||||
// 分组:新分组名非空时优先,否则取下拉所选(首项=无分组)
|
||||
@@ -558,7 +559,7 @@ impl ConnectionForm {
|
||||
// WS/WSS 依赖 path 建立升级请求,缺失时连接必然失败,保存前拦截
|
||||
let path = self.val(&self.path, cx).trim().to_string();
|
||||
if transport.is_websocket() && path.is_empty() {
|
||||
return Err(self.fail("path", "WS/WSS 传输下 WS 路径不能为空"));
|
||||
return Err(self.fail("path", i18n::t("WS/WSS 传输下 WS 路径不能为空")));
|
||||
}
|
||||
|
||||
let ssl = SslConfig {
|
||||
@@ -571,7 +572,7 @@ impl ConnectionForm {
|
||||
if transport.is_tls()
|
||||
&& ssl.client_cert_file.is_empty() != ssl.client_key_file.is_empty()
|
||||
{
|
||||
return Err(self.fail("ssl_client_cert", "客户端证书与客户端密钥必须同时填写"));
|
||||
return Err(self.fail("ssl_client_cert", i18n::t("客户端证书与客户端密钥必须同时填写")));
|
||||
}
|
||||
// 证书文件不存在时握手才报错太晚,保存前给出字段级提示
|
||||
if transport.is_tls() {
|
||||
@@ -581,7 +582,7 @@ impl ConnectionForm {
|
||||
("ssl_client_key", "客户端私钥", &ssl.client_key_file),
|
||||
] {
|
||||
if !p.is_empty() && !std::path::Path::new(p).exists() {
|
||||
return Err(self.fail(key, format!("{label}路径不存在:{p}")));
|
||||
return Err(self.fail(key, i18n::tf("{label}路径不存在:{p}", &[("label", &i18n::t(label)), ("p", &p)])));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -590,13 +591,13 @@ impl ConnectionForm {
|
||||
// 与其它字段一致:trim 后入库
|
||||
let topic = self.val(&self.will_topic, cx).trim().to_string();
|
||||
if topic.is_empty() {
|
||||
return Err(self.fail("will_topic", "遗嘱主题不能为空"));
|
||||
return Err(self.fail("will_topic", i18n::t("遗嘱主题不能为空")));
|
||||
}
|
||||
// 通配符只用于订阅过滤器;出现在 PUBLISH 主题上会被 broker 直接拒绝
|
||||
if topic.contains('+') || topic.contains('#') {
|
||||
return Err(self.fail(
|
||||
"will_topic",
|
||||
"遗嘱主题不能包含通配符 + 或 #",
|
||||
i18n::t("遗嘱主题不能包含通配符 + 或 #"),
|
||||
));
|
||||
}
|
||||
let opt_str = |v: String| {
|
||||
@@ -702,7 +703,7 @@ impl ConnectionForm {
|
||||
)
|
||||
.child(
|
||||
Button::new(id)
|
||||
.label("浏览…")
|
||||
.label(i18n::t("浏览…"))
|
||||
.outline()
|
||||
.small()
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
@@ -722,7 +723,7 @@ impl ConnectionForm {
|
||||
let (tx, rx) = smol::channel::bounded::<Option<std::path::PathBuf>>(1);
|
||||
std::thread::spawn(move || {
|
||||
let picked = rfd::FileDialog::new()
|
||||
.set_title("选择证书 / 密钥文件")
|
||||
.set_title(i18n::t("选择证书 / 密钥文件"))
|
||||
.add_filter("证书 / 密钥 (PEM)", &["pem", "crt", "cer", "key", "der"])
|
||||
.pick_file();
|
||||
let _ = tx.send_blocking(picked);
|
||||
@@ -877,39 +878,40 @@ impl Render for ConnectionForm {
|
||||
.iter()
|
||||
.filter(|on| **on)
|
||||
.count();
|
||||
let ssl_hint = (ssl_count > 0).then(|| SharedString::from(format!("已配置 {ssl_count} 项")));
|
||||
let ssl_hint = (ssl_count > 0).then(|| SharedString::from(i18n::tf("已配置 {n} 项", &[("n", &ssl_count.to_string())])));
|
||||
let mqtt5_hint =
|
||||
(mqtt5_count > 0).then(|| SharedString::from(format!("已配置 {mqtt5_count} 项")));
|
||||
(mqtt5_count > 0).then(|| SharedString::from(i18n::tf("已配置 {n} 项", &[("n", &mqtt5_count.to_string())])));
|
||||
v_flex()
|
||||
.gap_3()
|
||||
.max_h(px(560.))
|
||||
.overflow_y_scrollbar()
|
||||
.pr_1()
|
||||
// ── 基本信息(始终展开) ──
|
||||
.child(section("基本信息", border, card_bg,
|
||||
.child(section(i18n::t("基本信息"), 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.host), true, err("host"), danger)))
|
||||
.child(div().w(px(120.)).child(field_ex("端口", Input::new(&self.port), true, err("port"), danger)))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field_ex(i18n::t("主机"), Input::new(&self.host), true, err("host"), danger)))
|
||||
.child(div().w(px(120.)).child(field_ex(i18n::t(i18n::t("端口")), 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.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(div().flex_1().min_w(px(0.)).child(field_ex(i18n::t("名称"), Input::new(&self.name), true, err("name"), danger)))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field(i18n::t("协议"), Select::new(&self.protocol))))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field(i18n::t("传输"), 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(i18n::t("分组"), Select::new(&self.group_select))))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field(i18n::t("新建分组"), 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)))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field_ex(i18n::t("Client ID"), Input::new(&self.client_id), true, err("client_id"), danger)))
|
||||
// 一键换一个随机 Client ID,与 ConnectionConfig::new 的生成逻辑一致
|
||||
.child(
|
||||
Button::new("cid-regen")
|
||||
.icon(IconName::RefreshCw)
|
||||
.ghost()
|
||||
.tooltip("重新生成 Client ID")
|
||||
.tooltip(i18n::t("重新生成 Client ID"))
|
||||
.accessibility_label(i18n::t("重新生成 Client ID"))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
let cid = crate::model::generate_client_id();
|
||||
this.client_id
|
||||
@@ -919,48 +921,48 @@ impl Render for ConnectionForm {
|
||||
)
|
||||
// WS 路径仅对 WebSocket/WSS 有意义,按需显示;该传输下必填
|
||||
.when(is_ws, |w| w.child(
|
||||
div().flex_1().min_w(px(0.)).child(field_ex("WS 路径", Input::new(&self.path), true, err("path"), danger))
|
||||
div().flex_1().min_w(px(0.)).child(field_ex(i18n::t("WS 路径"), Input::new(&self.path), true, err("path"), danger))
|
||||
))
|
||||
)
|
||||
))
|
||||
// ── 认证与心跳 ──
|
||||
.child(self.collapsible_section(
|
||||
"sec-auth", "认证与心跳", None, false, self.show_auth,
|
||||
"sec-auth", i18n::t("认证与心跳"), None, false, self.show_auth,
|
||||
|f| f.show_auth = !f.show_auth, border, card_bg,
|
||||
v_flex().gap_2()
|
||||
.child(h_flex().gap_2().w_full()
|
||||
.child(div().flex_1().min_w(px(0.)).child(field("用户名", Input::new(&self.username))))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field("密码", Input::new(&self.password).mask_toggle())))
|
||||
.child(div().w(px(110.)).child(field_ex("Keep Alive (秒)", Input::new(&self.keep_alive), true, err("keep_alive"), danger)))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field(i18n::t("用户名"), Input::new(&self.username))))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field(i18n::t("密码"), Input::new(&self.password).mask_toggle())))
|
||||
.child(div().w(px(110.)).child(field_ex(i18n::t("Keep Alive (秒)"), Input::new(&self.keep_alive), true, err("keep_alive"), danger)))
|
||||
)
|
||||
.child(h_flex().gap_2().w_full()
|
||||
.child(div().w(px(150.)).child(field_ex("连接超时 (秒)", Input::new(&self.conn_timeout), false, err("conn_timeout"), danger)))
|
||||
.child(div().w(px(170.)).child(field_ex("最大重连次数 (0=无限)", Input::new(&self.max_reconnect), false, err("max_reconnect"), danger)))
|
||||
.child(div().w(px(150.)).child(field_ex(i18n::t("连接超时 (秒)"), Input::new(&self.conn_timeout), false, err("conn_timeout"), danger)))
|
||||
.child(div().w(px(170.)).child(field_ex(i18n::t("最大重连次数 (0=无限)"), Input::new(&self.max_reconnect), false, err("max_reconnect"), danger)))
|
||||
)
|
||||
.child(self.switch_row("clean_start", "Clean Start / Clean Session", self.clean_start, cx))
|
||||
.child(self.switch_row("auto_reconnect", "断线自动重连", self.auto_reconnect, cx))
|
||||
.child(self.switch_row("auto_resubscribe", "连接后自动恢复订阅", self.auto_resubscribe, cx))
|
||||
.child(self.switch_row("auto_connect", "启动时自动连接", self.auto_connect, cx)),
|
||||
.child(self.switch_row("clean_start", i18n::t("Clean Start / Clean Session"), self.clean_start, cx))
|
||||
.child(self.switch_row("auto_reconnect", i18n::t("断线自动重连"), self.auto_reconnect, cx))
|
||||
.child(self.switch_row("auto_resubscribe", i18n::t("连接后自动恢复订阅"), self.auto_resubscribe, cx))
|
||||
.child(self.switch_row("auto_connect", i18n::t("启动时自动连接"), self.auto_connect, cx)),
|
||||
cx,
|
||||
))
|
||||
// ── SSL/TLS(仅 TLS/WSS 显示) ──
|
||||
.when(is_tls, |w| w.child(self.collapsible_section(
|
||||
"sec-ssl", "SSL/TLS", ssl_hint, true, self.show_ssl,
|
||||
"sec-ssl", i18n::t("SSL/TLS"), ssl_hint, true, self.show_ssl,
|
||||
|f| f.show_ssl = !f.show_ssl, border, card_bg,
|
||||
v_flex().gap_2()
|
||||
.child(self.cert_path_row(
|
||||
"ssl-ca-browse", "CA 证书 (PEM)", self.ssl_ca.clone(),
|
||||
"ssl-ca-browse", i18n::t("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(),
|
||||
"ssl-cert-browse", i18n::t("客户端证书 (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(),
|
||||
"ssl-key-browse", i18n::t("客户端密钥 (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)),
|
||||
.child(self.switch_row("ssl_ignore_ca", i18n::t("忽略 CA 校验(不验证服务器证书)"), self.ssl_ignore_ca, cx)),
|
||||
cx,
|
||||
)))
|
||||
// ── MQTT 5 高级属性(仅 v5 协议显示) ──
|
||||
@@ -968,29 +970,29 @@ impl Render for ConnectionForm {
|
||||
"sec-mqtt5", "MQTT 5 属性", mqtt5_hint, true, self.show_mqtt5,
|
||||
|f| f.show_mqtt5 = !f.show_mqtt5, border, card_bg,
|
||||
h_flex().gap_2().w_full()
|
||||
.child(div().flex_1().min_w(px(0.)).child(field_ex("会话过期间隔(秒)", Input::new(&self.session_expiry), false, err("session_expiry"), danger)))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field_ex("接收最大值", Input::new(&self.receive_max), false, err("receive_max"), danger)))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field_ex("最大报文长度", Input::new(&self.max_packet), false, err("max_packet"), danger)))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field_ex("主题别名上限", Input::new(&self.topic_alias), false, err("topic_alias"), danger))),
|
||||
.child(div().flex_1().min_w(px(0.)).child(field_ex(i18n::t("会话过期间隔(秒)"), Input::new(&self.session_expiry), false, err("session_expiry"), danger)))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field_ex(i18n::t("接收最大值"), Input::new(&self.receive_max), false, err("receive_max"), danger)))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field_ex(i18n::t("最大报文长度"), Input::new(&self.max_packet), false, err("max_packet"), danger)))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field_ex(i18n::t("主题别名上限"), Input::new(&self.topic_alias), false, err("topic_alias"), danger))),
|
||||
cx,
|
||||
)))
|
||||
// ── 遗嘱 ──
|
||||
.child(self.collapsible_section(
|
||||
"sec-will", "遗嘱消息 (Last Will)",
|
||||
self.will_enabled.then(|| "已启用".into()), true, self.show_will,
|
||||
"sec-will", i18n::t("遗嘱消息 (Last Will)"),
|
||||
self.will_enabled.then(|| i18n::t("已启用").into()), true, self.show_will,
|
||||
|f| f.show_will = !f.show_will, border, card_bg,
|
||||
v_flex().gap_2()
|
||||
.child(self.switch_row("will_enabled", "启用遗嘱消息", self.will_enabled, cx))
|
||||
.child(self.switch_row("will_enabled", i18n::t("启用遗嘱消息"), self.will_enabled, cx))
|
||||
.when(self.will_enabled, |w| w
|
||||
.child(h_flex().gap_2()
|
||||
.child(div().flex_1().min_w(px(0.)).child(field_ex("遗嘱主题", Input::new(&self.will_topic), true, err("will_topic"), danger)))
|
||||
.child(div().w(px(130.)).child(field("QoS", Select::new(&self.will_qos))))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field_ex(i18n::t("遗嘱主题"), Input::new(&self.will_topic), true, err("will_topic"), danger)))
|
||||
.child(div().w(px(130.)).child(field(i18n::t("QoS"), Select::new(&self.will_qos))))
|
||||
)
|
||||
.child(field("遗嘱内容", Input::new(&self.will_payload)))
|
||||
.child(field(i18n::t("遗嘱内容"), Input::new(&self.will_payload)))
|
||||
// v5 遗嘱属性;3.1.1 连接会忽略这两项
|
||||
.child(h_flex().gap_2().w_full()
|
||||
.child(div().flex_1().min_w(px(0.)).child(field("内容类型 (Content Type)", Input::new(&self.will_content_type))))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field("响应主题 (Response Topic)", Input::new(&self.will_response_topic))))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field(i18n::t("内容类型 (Content Type)"), Input::new(&self.will_content_type))))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field(i18n::t("响应主题 (Response Topic)"), Input::new(&self.will_response_topic))))
|
||||
)
|
||||
.child(self.switch_row("will_retain", "Retain", self.will_retain, cx))
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user