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
+122
View File
@@ -0,0 +1,122 @@
//! key/value 列表编辑器(MQTT 5 用户属性等)。
use gpui_kit::component::button::{ Button, ButtonVariants as _ };
use gpui_kit::component::input::{ Input, InputState };
use gpui_kit::component::{ h_flex, v_flex, Disableable as _, Sizable as _ };
use crate::ui::IconName;
use gpui_kit::{ App, AppContext as _, Context, Entity, InteractiveElement as _, IntoElement, ParentElement as _, Render, SharedString, Styled as _, Window, div, px };
/// MQTT 5 用户属性等 key/value 列表编辑器。
pub struct KvEditor {
id_prefix: SharedString,
pub rows: Vec<(Entity<InputState>, Entity<InputState>)>,
}
impl KvEditor {
pub fn new(
id_prefix: impl Into<SharedString>,
pairs: &[(String, String)],
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let mut editor = Self {
id_prefix: id_prefix.into(),
rows: Vec::new(),
};
for (k, v) in pairs {
editor.push(Some((k.as_str(), v.as_str())), window, cx);
}
if editor.rows.is_empty() {
editor.push(None, window, cx);
}
editor
}
fn push(&mut self, initial: Option<(&str, &str)>, window: &mut Window, cx: &mut Context<Self>) {
let key = cx.new(|cx| {
let mut s = InputState::new(window, cx).placeholder("key");
if let Some((k, _)) = initial {
s.set_value(k, window, cx);
}
s
});
let value = cx.new(|cx| {
let mut s = InputState::new(window, cx).placeholder("value");
if let Some((_, v)) = initial {
s.set_value(v, window, cx);
}
s
});
self.rows.push((key, value));
}
pub fn pairs(&self, cx: &App) -> Vec<(String, String)> {
// key 统一 trim 并去重(保留最后一条):否则带首尾空白的 key
// 永远无法被 {{key}} 模板引用,重复 key 也只有一条能生效。
let mut out: Vec<(String, String)> = Vec::new();
for (k, v) in self
.rows
.iter()
.map(|(k, v)| (k.read(cx).value().to_string(), v.read(cx).value().to_string()))
{
let k = k.trim().to_string();
if k.is_empty() {
continue;
}
match out.iter_mut().find(|(ek, _)| *ek == k) {
Some(slot) => slot.1 = v,
None => out.push((k, v)),
}
}
out
}
}
impl Render for KvEditor {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let prefix = self.id_prefix.clone();
let count = self.rows.len();
let mut rows = v_flex().gap_1();
for (i, (key, value)) in self.rows.iter().cloned().enumerate() {
rows = rows.child(
h_flex()
.id(SharedString::from(format!("kv-row-{prefix}-{i}")))
.gap_1()
.child(div().flex_1().min_w(px(0.)).child(Input::new(&key)))
.child(div().flex_1().min_w(px(0.)).child(Input::new(&value)))
.child(
Button::new(SharedString::from(format!("kv-del-{prefix}-{i}")))
.icon(IconName::Close)
.ghost()
.xsmall()
.disabled(count <= 1)
.on_click(cx.listener(move |this, _, _, cx| {
if this.rows.len() > 1 {
this.rows.remove(i);
cx.notify();
}
})),
),
);
}
v_flex()
.gap_2()
.child(rows)
.child(
Button::new(SharedString::from(format!("kv-add-{prefix}")))
.icon(IconName::Plus)
.label("添加属性")
.ghost()
.xsmall()
.on_click(cx.listener(|this, _, window, cx| {
this.push(None, window, cx);
cx.notify();
})),
)
}
}
// ─── 时间格式化 ──────────────────────────────────────────────────────────────