Skip to main content

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}