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: "animate-page-enter w-full max-w-7xl mx-auto space-y-6",
73            // 页面标题
74            div { class: "flex flex-col sm:flex-row sm:items-center justify-between gap-4 pb-6 border-b border-[var(--color-paper-border)]/70 mb-6",
75                div {
76                    h1 { class: "text-3xl sm:text-4xl font-extrabold tracking-tight text-[var(--color-paper-primary)]",
77                        "系统面板"
78                    }
79                    p { class: "text-sm text-[var(--color-paper-secondary)] mt-1.5",
80                        "数据库健康状态、服务器性能指标与系统诊断中心"
81                    }
82                }
83                div { class: "flex items-center gap-2.5",
84                    div { class: "inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-full text-xs font-mono bg-[var(--color-paper-entry)] text-[var(--color-paper-secondary)] border border-[var(--color-paper-border)]/70 shadow-2xs",
85                        span { class: "w-1.5 h-1.5 rounded-full bg-[var(--color-paper-accent)]" }
86                        span { "诊断引擎在线" }
87                    }
88                }
89            }
90            // 顶部 tab 切换栏:复用公共 FilterTabs 组件(String API,经 as_str/from_str 桥接枚举)。
91            // 视觉与评论页一致:平滑滑动指示条 + 选中文字 text-paper-primary。
92            FilterTabs {
93                items: vec![
94                    ("db_status", "数据库状态"),
95                    ("server_status", "服务器状态"),
96                    ("sql_console", "SQL 控制台"),
97                    ("export", "数据导出"),
98                    ("backup", "备份恢复"),
99                ],
100                active_value: active_tab().as_str().to_string(),
101                on_change: move |v: String| {
102                    // 未知 key fallback 到默认 tab,保证状态始终有效。
103                    active_tab.set(SystemTab::from_str(&v).unwrap_or(SystemTab::DbStatus));
104                },
105            }
106
107            // tab 内容
108            // 经 std::iter::once 包一层 keyed remount:Dioxus 0.7 对非列表元素的裸 key
109            // 会忽略(见 post_detail.rs 头文档约定 #5、settings/mod.rs 同款修复)。
110            // once 让 active_tab() 变化时真正卸载/重建内层 div,既保证 hook slot
111            // 不复用(DelayedSkeleton 的 visible 信号不残留),又让 animate-section-enter
112            // 在每次切 tab 时重播。
113            for tab_key in std::iter::once(active_tab().as_str()) {
114                div { key: "{tab_key}", class: "animate-section-enter",
115                    match active_tab() {
116                        SystemTab::DbStatus => rsx! {
117                            DbStatusTab {}
118                        },
119                        SystemTab::ServerStatus => rsx! {
120                            ServerStatusTab {}
121                        },
122                        SystemTab::SqlConsole => rsx! {
123                            SqlConsoleTab {}
124                        },
125                        SystemTab::Export => rsx! {
126                            ExportTab {}
127                        },
128                        SystemTab::Backup => rsx! {
129                            BackupTab {}
130                        },
131                    }
132                }
133            }
134        }
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::SystemTab;
141
142    #[test]
143    fn as_str_roundtrips_all_variants() {
144        // 每个变体经 as_str -> from_str 必须还原为自身。
145        for tab in [
146            SystemTab::DbStatus,
147            SystemTab::ServerStatus,
148            SystemTab::SqlConsole,
149            SystemTab::Export,
150            SystemTab::Backup,
151        ] {
152            let s = tab.as_str();
153            assert_eq!(
154                SystemTab::from_str(s),
155                Ok(tab),
156                "roundtrip failed for {tab:?}"
157            );
158        }
159    }
160
161    #[test]
162    fn as_str_returns_stable_keys() {
163        // 字符串 key 必须稳定(改 key 会破坏 URL/调试/未来持久化),锁定之。
164        assert_eq!(SystemTab::DbStatus.as_str(), "db_status");
165        assert_eq!(SystemTab::ServerStatus.as_str(), "server_status");
166        assert_eq!(SystemTab::SqlConsole.as_str(), "sql_console");
167        assert_eq!(SystemTab::Export.as_str(), "export");
168        assert_eq!(SystemTab::Backup.as_str(), "backup");
169    }
170
171    #[test]
172    fn from_str_rejects_unknown_and_empty() {
173        assert!(SystemTab::from_str("nonsense").is_err());
174        assert!(SystemTab::from_str("").is_err());
175        // 大小写敏感:不接受大写变体,避免歧义。
176        assert!(SystemTab::from_str("DbStatus").is_err());
177    }
178}