Skip to main content

yggdrasil/pages/admin/system/
mod.rs

1//! 后台系统管理页面(数据库 + 服务器状态 + SQL 控制台 + 导出 + 备份)。
2//!
3//! 用顶部 tab 切换 5 个功能,tab 状态用 `use_signal`(不深链 / 不走分页路由)。
4//! 各 tab 拆分为独立子模块,状态完全独立、互不共享——切换 tab 时父组件用 `key`
5//! 强制卸载旧 tab 组件,其内部 signal 随之销毁。
6
7mod backup;
8mod db_status;
9mod export;
10mod server_status;
11mod sql_console;
12
13use backup::BackupTab;
14use db_status::DbStatusTab;
15use dioxus::prelude::*;
16use export::ExportTab;
17use server_status::ServerStatusTab;
18use sql_console::SqlConsoleTab;
19
20use crate::components::ui::FilterTabs;
21
22/// 系统管理的 5 个功能 tab。
23#[derive(Clone, Copy, PartialEq, Debug)]
24enum SystemTab {
25    /// 数据库运行状态(表/连接/死元组/迁移版本)。
26    DbStatus,
27    /// 服务器状态(应用内 + 主机层 CPU/内存/磁盘)。
28    ServerStatus,
29    /// SQL 控制台(全读写 + 护栏)。
30    SqlConsole,
31    /// 数据导出(SQL/CSV 流式下载)。
32    Export,
33    /// 备份恢复(pg_dump + 任务进度)。
34    Backup,
35}
36
37impl SystemTab {
38    /// 变体 → 稳定字符串 key(用于与基于 String 的 `FilterTabs` 组件桥接)。
39    /// 改这些 key 会破坏潜在的持久化/调试场景,见 `from_str` 的反向映射。
40    fn as_str(&self) -> &'static str {
41        match self {
42            SystemTab::DbStatus => "db_status",
43            SystemTab::ServerStatus => "server_status",
44            SystemTab::SqlConsole => "sql_console",
45            SystemTab::Export => "export",
46            SystemTab::Backup => "backup",
47        }
48    }
49
50    /// 字符串 key → 变体。未知/空串返回 Err(调用方 fallback 到默认 tab)。
51    /// 与 `as_str` 严格对应;大小写敏感。
52    fn from_str(s: &str) -> Result<SystemTab, &'static str> {
53        match s {
54            "db_status" => Ok(SystemTab::DbStatus),
55            "server_status" => Ok(SystemTab::ServerStatus),
56            "sql_console" => Ok(SystemTab::SqlConsole),
57            "export" => Ok(SystemTab::Export),
58            "backup" => Ok(SystemTab::Backup),
59            _ => Err("unknown tab key"),
60        }
61    }
62}
63
64/// 系统管理入口组件。
65#[component]
66pub fn System() -> Element {
67    // tab 状态:默认进第一个 tab(数据库状态)。用 signal 而非 URL query——
68    // tab 切换无需深链/书签,避免新增路由变体。
69    let mut active_tab = use_signal(|| SystemTab::DbStatus);
70
71    rsx! {
72        div { class: "w-full max-w-7xl mx-auto space-y-6",
73            // 页面标题
74            div { class: "flex flex-col md:flex-row md:items-end justify-between gap-6 pb-6 border-b border-[var(--color-paper-border)] mb-6",
75                div {
76                    h1 { class: "text-4xl font-extrabold tracking-tight text-[var(--color-paper-primary)]",
77                        "系统面板"
78                    }
79                    p { class: "text-base text-[var(--color-paper-secondary)] mt-2",
80                        "数据库与服务器诊断"
81                    }
82                }
83            }
84
85            // 顶部 tab 切换栏:复用公共 FilterTabs 组件(String API,经 as_str/from_str 桥接枚举)。
86            // 视觉与评论页一致:平滑滑动指示条 + 选中文字 text-paper-primary。
87            FilterTabs {
88                items: vec![
89                    ("db_status", "数据库状态"),
90                    ("server_status", "服务器状态"),
91                    ("sql_console", "SQL 控制台"),
92                    ("export", "数据导出"),
93                    ("backup", "备份恢复"),
94                ],
95                active_value: active_tab().as_str().to_string(),
96                on_change: move |v: String| {
97                    // 未知 key fallback 到默认 tab,保证状态始终有效。
98                    active_tab.set(SystemTab::from_str(&v).unwrap_or(SystemTab::DbStatus));
99                },
100            }
101
102            // tab 内容
103            // key 保证切换 tab 时 Dioxus 完全卸载旧组件、重新挂载新组件,
104            // 避免 hook slot 复用导致 DelayedSkeleton 的 visible 信号残留为 true。
105            div { key: "{active_tab().as_str()}",
106                match active_tab() {
107                    SystemTab::DbStatus => rsx! {
108                        DbStatusTab {}
109                    },
110                    SystemTab::ServerStatus => rsx! {
111                        ServerStatusTab {}
112                    },
113                    SystemTab::SqlConsole => rsx! {
114                        SqlConsoleTab {}
115                    },
116                    SystemTab::Export => rsx! {
117                        ExportTab {}
118                    },
119                    SystemTab::Backup => rsx! {
120                        BackupTab {}
121                    },
122                }
123            }
124        }
125    }
126}
127
128/// 字节数 → 人类可读(如 1.2 MB)。db_status / server_status / backup 三个 tab 共用。
129pub(super) fn format_bytes(bytes: i64) -> String {
130    const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
131    let mut size = bytes as f64;
132    let mut unit = 0;
133    while size.abs() >= 1024.0 && unit < UNITS.len() - 1 {
134        size /= 1024.0;
135        unit += 1;
136    }
137    if unit == 0 {
138        format!("{} {}", bytes, UNITS[0])
139    } else {
140        format!("{:.2} {}", size, UNITS[unit])
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::SystemTab;
147
148    #[test]
149    fn as_str_roundtrips_all_variants() {
150        // 每个变体经 as_str -> from_str 必须还原为自身。
151        for tab in [
152            SystemTab::DbStatus,
153            SystemTab::ServerStatus,
154            SystemTab::SqlConsole,
155            SystemTab::Export,
156            SystemTab::Backup,
157        ] {
158            let s = tab.as_str();
159            assert_eq!(
160                SystemTab::from_str(s),
161                Ok(tab),
162                "roundtrip failed for {tab:?}"
163            );
164        }
165    }
166
167    #[test]
168    fn as_str_returns_stable_keys() {
169        // 字符串 key 必须稳定(改 key 会破坏 URL/调试/未来持久化),锁定之。
170        assert_eq!(SystemTab::DbStatus.as_str(), "db_status");
171        assert_eq!(SystemTab::ServerStatus.as_str(), "server_status");
172        assert_eq!(SystemTab::SqlConsole.as_str(), "sql_console");
173        assert_eq!(SystemTab::Export.as_str(), "export");
174        assert_eq!(SystemTab::Backup.as_str(), "backup");
175    }
176
177    #[test]
178    fn from_str_rejects_unknown_and_empty() {
179        assert!(SystemTab::from_str("nonsense").is_err());
180        assert!(SystemTab::from_str("").is_err());
181        // 大小写敏感:不接受大写变体,避免歧义。
182        assert!(SystemTab::from_str("DbStatus").is_err());
183    }
184}