refactor(ui): 拆分 app/connection_view/widgets 为子组件,对话框归拢 dialogs/

- ui/app/ 拆为门面、events(引擎事件泵)、actions(用户动作)、io(导入导出)、
  theme(主题)、titlebar/sidebar/main_area(渲染分区)
- ui/connection_view/ 拆为门面、subscribe、messages、publish、vars、util(纯函数)
- ui/widgets/ 拆为 select(可搜索下拉)、kv_editor、common(字段行/状态/时间)
- 五个对话框归拢 ui/dialogs/,linear-theme.json 随 theme.rs 迁入 ui/app/
- clippy 0 警告,全部路径经门面保持稳定
This commit is contained in:
2026-09-29 22:20:53 +08:00
parent b5089f5898
commit bc9f55c889
30 changed files with 4884 additions and 4652 deletions
+143
View File
@@ -0,0 +1,143 @@
//! 连接配置导入/导出(文件对话框 + JSON 序列化)。
use std::path::PathBuf;
use gpui_kit::component::{ notification::Notification, };
use gpui_kit::{ Context, Window, };
use super::*;
impl MqttXApp {
/// 数据目录(设置对话框展示 / 打开用)。
pub fn data_dir(&self) -> PathBuf {
self.storage.dir().to_path_buf()
}
/// 日志目录(设置对话框展示 / 打开用)。
pub fn log_dir(&self) -> PathBuf {
self.storage.log_dir()
}
// ── 连接导入 / 导出 ────────────────────────────────────────────────────
/// 在后台线程弹出阻塞式原生文件对话框,完成后回主线程执行 `done`。
/// 对话框进行中置位,防止重复弹出。
fn spawn_file_dialog<R, F, D>(
&mut self,
work: F,
done: D,
window: &mut Window,
cx: &mut Context<Self>,
) where
F: FnOnce() -> R + Send + 'static,
R: Send + 'static,
D: FnOnce(&mut Self, R, &mut Window, &mut Context<Self>) + 'static,
{
if self.file_dialog_open {
return;
}
self.file_dialog_open = true;
// rfd 的阻塞对话框不可占用 UI 线程:结果经 smol 通道送回异步任务
let (tx, rx) = smol::channel::bounded::<R>(1);
std::thread::spawn(move || {
let _ = tx.send_blocking(work());
});
let weak = cx.entity().downgrade();
cx.spawn_in(window, async move |_this, cx: &mut gpui_kit::AsyncWindowContext| {
let result = rx.recv().await.ok();
weak.update_in(cx, |app, window, cx| {
app.file_dialog_open = false;
if let Some(result) = result {
done(app, result, window, cx);
}
})
.ok();
})
.detach();
}
/// 导出全部连接为 JSON(原生保存对话框,默认文件名 connections-export.json)。
pub fn export_connections(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let dir = self.storage.dir().to_path_buf();
self.spawn_file_dialog(
move || {
rfd::FileDialog::new()
.set_title("导出连接")
.set_directory(dir)
.set_file_name("connections-export.json")
.add_filter("JSON", &["json"])
.save_file()
},
|app, path, window, cx| {
let Some(path) = path else {
return; // 用户取消
};
let count = app.connections.len();
match crate::store::export_connections_to(&path, &app.connections) {
Ok(()) => window.push_notification(
Notification::success(format!(
"已导出 {count} 条连接到 {}",
path.display()
)),
cx,
),
Err(e) => window.push_notification(Notification::error(e), cx),
}
},
window,
cx,
);
}
/// 导入连接:按 id 去重(同 id 跳过),新 id 保留文件原值;读取解析在后台线程完成。
pub fn import_connections(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let dir = self.storage.dir().to_path_buf();
self.spawn_file_dialog(
move || {
let Some(path) = rfd::FileDialog::new()
.set_title("导入连接")
.set_directory(dir)
.add_filter("JSON", &["json"])
.pick_file()
else {
return Ok(None); // 用户取消
};
crate::store::import_connections_from(&path).map(Some)
},
|app, result, window, cx| match result {
Err(e) => {
window.push_notification(Notification::error(e), cx);
}
Ok(None) => {}
Ok(Some(items)) => {
let mut existing: std::collections::HashSet<String> =
app.connections.iter().map(|c| c.id.clone()).collect();
let mut added = 0usize;
let mut skipped = 0usize;
for item in items {
if existing.contains(&item.id) {
skipped += 1;
} else {
// 记录已入库 id,导入文件内部的重复 id 也只收一条
existing.insert(item.id.clone());
app.connections.push(item);
added += 1;
}
}
if added > 0 {
app.storage.save_connections(&app.connections);
}
window.push_notification(
Notification::success(format!("导入 {added} 条,跳过 {skipped} 条")),
cx,
);
}
},
window,
cx,
);
}
// ── 对话框 ────────────────────────────────────────────────────────────
}