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
+160
View File
@@ -0,0 +1,160 @@
//! 模板变量面板:扫描 topic/payload 占位符并管理输入行。
use gpui_kit::component::button::{ Button };
use gpui_kit::component::input::{ Input, InputEvent, InputState };
use gpui_kit::component::{ h_flex, notification::Notification, v_flex, };
use crate::ui::IconName;
use gpui_kit::{ div, px, Context, IntoElement, Window, };
use crate::model::{ render_template, GlobalVariable, };
use super::*;
impl ConnectionView {
/// 输入框创建需要 Window,因此只能在按钮回调里调用(无法在 render 中同步)。
pub(super) fn sync_var_rows(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let topic = self.pub_topic.read(cx).value().to_string();
let payload = self.payload.read(cx).value().to_string();
let keys = extract_placeholder_keys(&topic, &payload);
if keys.is_empty() {
self.show_vars = false;
window.push_notification(
Notification::warning("主题/负载中没有 {{变量}} 引用"),
cx,
);
cx.notify();
return;
}
let vars = self.with_app(cx, |a| a.variables.clone()).unwrap_or_default();
// 移除已不存在的 key(保留仍存在的行,避免丢未保存的输入)
self.var_rows.retain(|(k, _, _)| keys.contains(k));
for key in keys {
if self.var_rows.iter().any(|(k, ..)| k == &key) {
continue;
}
let initial = vars
.iter()
.find(|v| v.key == key)
.map(|v| v.value.clone())
.unwrap_or_default();
let input = cx.new(|cx| {
let mut s = InputState::new(window, cx).placeholder("变量值");
// set_value 不会触发 InputEvent::Change,不会误写回
if !initial.is_empty() {
s.set_value(initial.as_str(), window, cx);
}
s
});
// 编辑即写回全局变量(含持久化),预览与发布随之生效
let key_for_sub = key.clone();
let sub = cx.subscribe(&input, move |this, entity, ev: &InputEvent, cx| {
if !matches!(ev, InputEvent::Change) {
return;
}
let val = entity.read(cx).value().to_string();
let key = key_for_sub.clone();
let Some(app) = this.app.upgrade() else {
return;
};
app.update(cx, |a, cx| {
match a.variables.iter_mut().find(|v| v.key == key) {
Some(v) => {
if v.value == val {
return;
}
v.value = val;
}
None => a.variables.push(GlobalVariable { key, value: val }),
}
let snapshot = a.variables.clone();
a.save_variables(snapshot);
cx.notify();
});
});
self.var_rows.push((key, input, sub));
}
self.show_vars = true;
cx.notify();
}
/// 变量绑定面板:逐 key 编辑 + 实时渲染预览。
pub(super) fn render_vars_panel(&self, cx: &mut Context<Self>) -> impl IntoElement {
let vars = self.with_app(cx, |a| a.variables.clone()).unwrap_or_default();
let topic_raw = self.pub_topic.read(cx).value().to_string();
let payload_raw = self.payload.read(cx).value().to_string();
let topic_rendered = render_template(&topic_raw, &vars);
let payload_rendered = render_template(&payload_raw, &vars);
let payload_preview: String = payload_rendered.chars().take(80).collect();
let muted = cx.theme().muted_foreground;
let mut rows = v_flex().gap_1();
for (key, input, _) in &self.var_rows {
rows = rows.child(
h_flex()
.gap_2()
.items_center()
.child(
div()
.w(px(160.))
.min_w(px(0.))
.text_xs()
.text_color(muted)
.child(format!("{{{{{key}}}}}")),
)
.child(div().flex_1().min_w(px(0.)).child(Input::new(input).small())),
);
}
v_flex()
.gap_2()
.w_full()
.child(
h_flex()
.gap_2()
.items_center()
.child(
div()
.text_xs()
.font_semibold()
.child(format!("变量({})", self.var_rows.len())),
)
.child(div().flex_1())
.child(
Button::new("vars-refresh")
.icon(IconName::RefreshCw)
.ghost()
.xsmall()
.tooltip("重新提取占位符")
.on_click(cx.listener(|this, _, window, cx| {
this.sync_var_rows(window, cx);
})),
)
.child(
Button::new("vars-collapse")
.icon(IconName::ChevronUp)
.ghost()
.xsmall()
.tooltip("收起")
.on_click(cx.listener(|this, _, _, cx| {
this.show_vars = false;
cx.notify();
})),
),
)
.child(rows)
.child(
div()
.text_xs()
.text_color(muted)
.child("内置:{{$ts}}(秒) {{$ts_ms}}(毫秒) {{$uuid}} · 发布时注入"),
)
.child(
div()
.text_xs()
.text_color(muted)
.child(format!("预览:{topic_rendered} | {payload_preview}")),
)
}
}