Merge branch 'feature/admin-database-system'
抽取 /admin/system 页面级导航 Tab 公共组件并迁移调用方: - feat(database): SystemTab::as_str/from_str 字符串桥接 + 单测 - feat(ui): Tabs 组件(与 FilterTabs 并列,页面导航语义,无动画) - refactor(admin): System 组件改用 Tabs,移除内联 tab 与临时 allow
This commit is contained in:
commit
95fb8d8698
@ -274,3 +274,46 @@ pub fn FilterTabs(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 页面级导航 Tab 组件。
|
||||
///
|
||||
/// 与 [`FilterTabs`] 的区别:
|
||||
/// - [`FilterTabs`]:**列表筛选**语义,带平滑滑动底部指示条动画(靠 WASM 端测量
|
||||
/// `offsetLeft/offsetWidth`)。适合「全部 / 待审核 / 已通过」这类同质数据过滤。
|
||||
/// - `Tabs`:**页面导航**语义,选中态用按钮自身 `border-b-2` 下划线(无动画)。
|
||||
/// 适合「数据库状态 / 服务器状态 / SQL 控制台」这类切换到结构完全不同的面板。
|
||||
///
|
||||
/// 因 Dioxus 0.7 `#[component]` 宏不支持泛型类型参数,value 用 `String`(与
|
||||
/// `FilterTabs` 一致);调用方负责枚举 ↔ String 的桥接(见 `SystemTab::as_str`)。
|
||||
///
|
||||
/// Props:
|
||||
/// - `items`:`(value, label)` 列表,`value` 是稳定字符串 key,`label` 是展示文案
|
||||
/// - `active_value`:当前选中项的 value
|
||||
/// - `on_change`:切换时回调,传入新选中项的 value
|
||||
#[component]
|
||||
pub fn Tabs(
|
||||
items: Vec<(&'static str, &'static str)>,
|
||||
active_value: String,
|
||||
on_change: EventHandler<String>,
|
||||
) -> Element {
|
||||
rsx! {
|
||||
div { class: "flex flex-wrap gap-1 border-b border-paper-border",
|
||||
for (value, label) in items {
|
||||
button {
|
||||
key: "{value}",
|
||||
class: "px-4 py-2 text-sm font-medium border-b-2 -mb-px transition-colors",
|
||||
class: if active_value == *value {
|
||||
"border-paper-accent text-paper-accent"
|
||||
} else {
|
||||
"border-transparent text-paper-secondary hover:text-paper-primary"
|
||||
},
|
||||
onclick: {
|
||||
let v = value.to_string();
|
||||
move |_| on_change.call(v.clone())
|
||||
},
|
||||
{label}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,8 @@
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
use crate::components::ui::Tabs;
|
||||
|
||||
/// 系统管理的 5 个功能 tab。
|
||||
#[derive(Clone, Copy, PartialEq, Debug)]
|
||||
enum SystemTab {
|
||||
@ -20,6 +22,33 @@ enum SystemTab {
|
||||
Backup,
|
||||
}
|
||||
|
||||
impl SystemTab {
|
||||
/// 变体 → 稳定字符串 key(用于与基于 String 的 `Tabs` 组件桥接)。
|
||||
/// 改这些 key 会破坏潜在的持久化/调试场景,见 `from_str` 的反向映射。
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
SystemTab::DbStatus => "db_status",
|
||||
SystemTab::ServerStatus => "server_status",
|
||||
SystemTab::SqlConsole => "sql_console",
|
||||
SystemTab::Export => "export",
|
||||
SystemTab::Backup => "backup",
|
||||
}
|
||||
}
|
||||
|
||||
/// 字符串 key → 变体。未知/空串返回 Err(调用方 fallback 到默认 tab)。
|
||||
/// 与 `as_str` 严格对应;大小写敏感。
|
||||
fn from_str(s: &str) -> Result<SystemTab, &'static str> {
|
||||
match s {
|
||||
"db_status" => Ok(SystemTab::DbStatus),
|
||||
"server_status" => Ok(SystemTab::ServerStatus),
|
||||
"sql_console" => Ok(SystemTab::SqlConsole),
|
||||
"export" => Ok(SystemTab::Export),
|
||||
"backup" => Ok(SystemTab::Backup),
|
||||
_ => Err("unknown tab key"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 系统管理入口组件。
|
||||
#[component]
|
||||
pub fn System() -> Element {
|
||||
@ -27,33 +56,24 @@ pub fn System() -> Element {
|
||||
// tab 切换无需深链/书签,避免新增路由变体。
|
||||
let mut active_tab = use_signal(|| SystemTab::DbStatus);
|
||||
|
||||
let tabs = [
|
||||
("数据库状态", SystemTab::DbStatus),
|
||||
("服务器状态", SystemTab::ServerStatus),
|
||||
("SQL 控制台", SystemTab::SqlConsole),
|
||||
("数据导出", SystemTab::Export),
|
||||
("备份恢复", SystemTab::Backup),
|
||||
];
|
||||
|
||||
rsx! {
|
||||
div { class: "space-y-6",
|
||||
h1 { class: "text-2xl font-bold text-paper-primary", "系统管理" }
|
||||
|
||||
// 顶部 tab 切换栏
|
||||
div { class: "flex flex-wrap gap-1 border-b border-paper-border",
|
||||
for (label, tab) in tabs {
|
||||
button {
|
||||
key: "{tab:?}",
|
||||
class: "px-4 py-2 text-sm font-medium border-b-2 -mb-px transition-colors",
|
||||
class: if active_tab() == tab {
|
||||
"border-paper-accent text-paper-accent"
|
||||
} else {
|
||||
"border-transparent text-paper-secondary hover:text-paper-primary"
|
||||
},
|
||||
onclick: move |_| active_tab.set(tab),
|
||||
{label}
|
||||
}
|
||||
}
|
||||
// 顶部 tab 切换栏:用公共 Tabs 组件(String API,经 as_str/from_str 桥接枚举)。
|
||||
Tabs {
|
||||
items: vec![
|
||||
("db_status", "数据库状态"),
|
||||
("server_status", "服务器状态"),
|
||||
("sql_console", "SQL 控制台"),
|
||||
("export", "数据导出"),
|
||||
("backup", "备份恢复"),
|
||||
],
|
||||
active_value: active_tab().as_str().to_string(),
|
||||
on_change: move |v: String| {
|
||||
// 未知 key fallback 到默认 tab,保证状态始终有效。
|
||||
active_tab.set(SystemTab::from_str(&v).unwrap_or(SystemTab::DbStatus));
|
||||
},
|
||||
}
|
||||
|
||||
// tab 内容
|
||||
@ -1393,3 +1413,41 @@ fn BackupRow(props: BackupRowProps) -> Element {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::SystemTab;
|
||||
|
||||
#[test]
|
||||
fn as_str_roundtrips_all_variants() {
|
||||
// 每个变体经 as_str -> from_str 必须还原为自身。
|
||||
for tab in [
|
||||
SystemTab::DbStatus,
|
||||
SystemTab::ServerStatus,
|
||||
SystemTab::SqlConsole,
|
||||
SystemTab::Export,
|
||||
SystemTab::Backup,
|
||||
] {
|
||||
let s = tab.as_str();
|
||||
assert_eq!(SystemTab::from_str(s), Ok(tab), "roundtrip failed for {tab:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn as_str_returns_stable_keys() {
|
||||
// 字符串 key 必须稳定(改 key 会破坏 URL/调试/未来持久化),锁定之。
|
||||
assert_eq!(SystemTab::DbStatus.as_str(), "db_status");
|
||||
assert_eq!(SystemTab::ServerStatus.as_str(), "server_status");
|
||||
assert_eq!(SystemTab::SqlConsole.as_str(), "sql_console");
|
||||
assert_eq!(SystemTab::Export.as_str(), "export");
|
||||
assert_eq!(SystemTab::Backup.as_str(), "backup");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str_rejects_unknown_and_empty() {
|
||||
assert!(SystemTab::from_str("nonsense").is_err());
|
||||
assert!(SystemTab::from_str("").is_err());
|
||||
// 大小写敏感:不接受大写变体,避免歧义。
|
||||
assert!(SystemTab::from_str("DbStatus").is_err());
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user