Skip to main content

yggdrasil/components/
empty_state.rs

1//! 空列表、无搜索结果等状态共用的图标、文案与行动入口。
2
3use leptos::prelude::*;
4
5use crate::components::mascot::Mascot;
6use crate::components::ui::{Icon, IconName, BTN_SECONDARY};
7
8/// 空状态行动按钮。跳转或重试由调用方负责。
9#[derive(Clone)]
10pub struct EmptyStateAction {
11    pub label: String,
12    pub onclick: Callback<()>,
13}
14
15impl EmptyStateAction {
16    /// 构造行动按钮;`onclick` 可直接传 `move |_| ..` 闭包。
17    pub fn new(label: impl Into<String>, onclick: impl Fn(()) + Send + Sync + 'static) -> Self {
18        Self {
19            label: label.into(),
20            onclick: Callback::new(onclick),
21        }
22    }
23}
24
25/// 展示线条图标或吉祥物、标题与说明,可选一个行动按钮。
26///
27/// 吉祥物优先于线条图标。图标默认为收件箱;无搜索结果可传 `IconName::SearchX`,缺图可传 `IconName::ImageOff`。
28#[component]
29pub fn EmptyState(
30    #[prop(into, default = "还没有文章".to_string())] title: String,
31    #[prop(optional, into)] description: String,
32    #[prop(default = IconName::Inbox)] icon: IconName,
33    #[prop(optional)] mascot: Option<Mascot>,
34    #[prop(optional)] action: Option<EmptyStateAction>,
35) -> impl IntoView {
36    view! {
37        <section class="empty-state">
38            {match mascot {
39                Some(m) => m.img("mascot empty-state-mascot").into_any(),
40                None => {
41                    view! {
42                        <span class="empty-state-icon">
43                            <Icon name=icon size=20/>
44                        </span>
45                    }
46                        .into_any()
47                }
48            }}
49            <h2 class="empty-state-title">{title}</h2>
50            {(!description.is_empty())
51                .then(|| view! { <p class="empty-state-description">{description}</p> })}
52            {action
53                .map(|act| {
54                    let onclick = act.onclick;
55                    view! {
56                        <button
57                            class=format!("{BTN_SECONDARY} empty-state-action")
58                            type="button"
59                            on:click=move |_| onclick.run(())
60                        >
61                            {act.label}
62                        </button>
63                    }
64                })}
65        </section>
66    }
67}