From ef75631889451e759fe5424d664d0abb6c8924b2 Mon Sep 17 00:00:00 2001 From: xfy Date: Tue, 30 Jun 2026 14:07:57 +0800 Subject: [PATCH 1/3] feat(database): add SystemTab::as_str/from_str string bridge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为后续用基于 String 的 Tabs 公共组件替换内联 tab 做准备。 as_str/from_str 是纯函数双向映射,带 roundtrip/稳定 key/拒绝未知输入三类单测。 --- src/pages/admin/system.rs | 68 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/src/pages/admin/system.rs b/src/pages/admin/system.rs index fa18b98..1201d64 100644 --- a/src/pages/admin/system.rs +++ b/src/pages/admin/system.rs @@ -20,6 +20,36 @@ enum SystemTab { Backup, } +// Task 3 起由 Tabs 组件经 SystemTab::as_str/from_str 消费;在此之前无生产调用方, +// 加 allow 抑制 -D warnings 的 dead_code(参考 src/api/auth.rs:427 的同类处理)。 +#[allow(dead_code)] +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 { + 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 { @@ -1393,3 +1423,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()); + } +} From ff7d9e1e46ad1afc4389fdb9cb5d795f9169ea53 Mon Sep 17 00:00:00 2001 From: xfy Date: Tue, 30 Jun 2026 14:16:36 +0800 Subject: [PATCH 2/3] feat(ui): add Tabs component for page-level navigation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 与 FilterTabs(列表筛选 + 滑动动画)并列,职责区分: - FilterTabs:同质数据过滤(全部/待审/已通过) - Tabs:切换到结构不同的面板(数据库/服务器/SQL 控制台) 选中态用按钮自身 border-b-2 下划线(无动画)。 API 与 FilterTabs 对齐(String value + EventHandler),便于调用方迁移。 --- src/components/ui.rs | 45 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/components/ui.rs b/src/components/ui.rs index b43ca5d..fd60bde 100644 --- a/src/components/ui.rs +++ b/src/components/ui.rs @@ -274,3 +274,48 @@ 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 +// Task 3 起由 /admin/system 页面消费;在此之前无调用方,加 allow 抑制 dead_code(参考 src/api/auth.rs:427)。 +#[allow(dead_code)] +#[component] +pub fn Tabs( + items: Vec<(&'static str, &'static str)>, + active_value: String, + on_change: EventHandler, +) -> 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} + } + } + } + } +} From bf91d18c4fef2d9a78c33464526a1a11cac2a4a8 Mon Sep 17 00:00:00 2001 From: xfy Date: Tue, 30 Jun 2026 14:29:05 +0800 Subject: [PATCH 3/3] refactor(admin): use shared Tabs component in /admin/system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除 System 组件内联的 tab 按钮,改调公共 Tabs 组件。 active_tab signal 经 SystemTab::as_str/from_str 与 Tabs 的 String API 桥接, match 穷举面板选择逻辑不变。视觉/行为零变化,只消除重复。 同时移除 Task 1/2 加的临时 #[allow(dead_code)](as_str/from_str/Tabs 现已有调用方)。 --- src/components/ui.rs | 2 -- src/pages/admin/system.rs | 42 +++++++++++++++------------------------ 2 files changed, 16 insertions(+), 28 deletions(-) diff --git a/src/components/ui.rs b/src/components/ui.rs index fd60bde..04ac33b 100644 --- a/src/components/ui.rs +++ b/src/components/ui.rs @@ -290,8 +290,6 @@ pub fn FilterTabs( /// - `items`:`(value, label)` 列表,`value` 是稳定字符串 key,`label` 是展示文案 /// - `active_value`:当前选中项的 value /// - `on_change`:切换时回调,传入新选中项的 value -// Task 3 起由 /admin/system 页面消费;在此之前无调用方,加 allow 抑制 dead_code(参考 src/api/auth.rs:427)。 -#[allow(dead_code)] #[component] pub fn Tabs( items: Vec<(&'static str, &'static str)>, diff --git a/src/pages/admin/system.rs b/src/pages/admin/system.rs index 1201d64..f16b8c1 100644 --- a/src/pages/admin/system.rs +++ b/src/pages/admin/system.rs @@ -5,6 +5,8 @@ use dioxus::prelude::*; +use crate::components::ui::Tabs; + /// 系统管理的 5 个功能 tab。 #[derive(Clone, Copy, PartialEq, Debug)] enum SystemTab { @@ -20,9 +22,6 @@ enum SystemTab { Backup, } -// Task 3 起由 Tabs 组件经 SystemTab::as_str/from_str 消费;在此之前无生产调用方, -// 加 allow 抑制 -D warnings 的 dead_code(参考 src/api/auth.rs:427 的同类处理)。 -#[allow(dead_code)] impl SystemTab { /// 变体 → 稳定字符串 key(用于与基于 String 的 `Tabs` 组件桥接)。 /// 改这些 key 会破坏潜在的持久化/调试场景,见 `from_str` 的反向映射。 @@ -57,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 内容