yggdrasil/components/empty_state.rs
1//! 空状态组件。
2//!
3//! 当列表(首页、标签、搜索等)无数据时展示:插画配图 + 标题 + 副文案,
4//! 可选行动按钮。视觉语言沿用项目 Forest 调色板(鼠尾草绿强调色)与
5//! Source Serif 4 衬线标题,留白克制,与首页 HomeInfo 标题区风格一致。
6//!
7//! 配图默认为「线条小狗」插画(`public/images/xiaotiaoxiaogou_01.webp`),
8//! 可通过 `image` prop 覆盖(如搜索页初始引导态用 `xiantiaoxiaogou_02`)。
9//! 通过 `<img>` 引用绝对路径,由 Dioxus 的静态资源服务直接返回。
10
11use dioxus::prelude::*;
12
13use crate::components::ui::BTN_PRIMARY;
14
15/// 空状态行动按钮。
16///
17/// `onclick` 而非直接绑定 `Route`:原子层不依赖 app 路由类型(见 `mod.rs` 分层约定),
18/// 跳转由调用方在回调内用 `dioxus::router::navigator().push(route)` 完成(与
19/// `router.rs`/`login.rs`/`not_found.rs`/`admin/write.rs` 的既有命令式导航约定一致)。
20#[derive(Props, Clone, PartialEq)]
21pub struct EmptyStateAction {
22 /// 按钮文案。
23 #[props(into)]
24 pub label: String,
25 /// 点击回调,调用方负责导航。
26 pub onclick: EventHandler<()>,
27}
28
29/// 空状态组件。
30///
31/// 默认渲染「线条小狗」配图;`title` / `description` / `image` / `action` 均可覆盖默认。
32/// 所有元素垂直居中,配图下方留白,与首页 `HomeInfo` 的居中布局对齐。
33///
34/// Props 由 `#[component]` 宏自动生成(均为可选)。
35#[component]
36pub fn EmptyState(
37 /// 主标题(通常为「还没有文章」之类)。
38 #[props(into, default = "还没有文章".to_string())]
39 title: String,
40 /// 副文案,说明当前状态或引导用户。
41 #[props(into, default = String::new())]
42 description: String,
43 /// 配图路径(缺省「线条小狗」持相机插画)。
44 #[props(into, default = "/images/xiaotiaoxiaogou_01.webp".to_string())]
45 image: String,
46 /// 可选的行动按钮。
47 #[props(default)]
48 action: Option<EmptyStateAction>,
49) -> Element {
50 rsx! {
51 div { class: "flex flex-col items-center justify-center text-center py-20 px-4 page-enter",
52 // 配图。
53 img {
54 class: "w-48 h-auto rounded-lg select-none dark:brightness-90",
55 src: "{image}",
56 alt: "线条小狗插画",
57 draggable: "false",
58 }
59 // 主标题:衬线字体,与首页 H1 风格呼应但更轻量。
60 h2 { class: "mt-8 text-2xl font-bold tracking-tight text-paper-primary",
61 "{title}"
62 }
63 // 副文案:次要色,限宽保证可读性。
64 if !description.is_empty() {
65 p { class: "mt-3 text-sm leading-relaxed text-paper-secondary max-w-md",
66 "{description}"
67 }
68 }
69 // 行动按钮:复用全站统一主操作按钮样式(BTN_PRIMARY)。
70 if let Some(act) = action {
71 button {
72 class: "{BTN_PRIMARY} mt-8",
73 onclick: move |_| act.onclick.call(()),
74 "{act.label}"
75 }
76 }
77 }
78 }
79}