adui_dioxus/components/
empty.rs

1use dioxus::prelude::*;
2
3/// Built-in image presets for the Empty component.
4#[derive(Clone, Debug, PartialEq)]
5pub enum EmptyImage {
6    /// Default illustration.
7    Default,
8    /// Minimal/simple illustration.
9    Simple,
10    /// Smaller footprint variant.
11    Small,
12    /// Custom image URL or label.
13    Custom(String),
14}
15
16/// Props for the Empty component.
17#[derive(Props, Clone, PartialEq)]
18pub struct EmptyProps {
19    /// Optional description text shown under the image.
20    #[props(optional)]
21    pub description: Option<String>,
22    /// Image preset or custom image.
23    #[props(optional)]
24    pub image: Option<EmptyImage>,
25    /// Extra class name applied to the root.
26    #[props(optional)]
27    pub class: Option<String>,
28    /// Inline style applied to the root.
29    #[props(optional)]
30    pub style: Option<String>,
31    /// Optional footer content rendered below the description (e.g. action buttons).
32    #[props(optional)]
33    pub footer: Option<Element>,
34}
35
36/// Ant Design flavored Empty component (MVP).
37#[component]
38pub fn Empty(props: EmptyProps) -> Element {
39    let EmptyProps {
40        description,
41        image,
42        class,
43        style,
44        footer,
45    } = props;
46
47    let mut classes = vec!["adui-empty".to_string()];
48    if let Some(extra) = class {
49        classes.push(extra);
50    }
51
52    // Mark small variant for styling when using `EmptyImage::Small`.
53    if matches!(image, Some(EmptyImage::Small)) {
54        classes.push("adui-empty-sm".to_string());
55    }
56
57    let class_attr = classes.join(" ");
58    let style_attr = style.unwrap_or_default();
59
60    let description_text = description.unwrap_or_else(|| "暂无数据".to_string());
61
62    // Render image content based on preset.
63    let image_node = match image.unwrap_or(EmptyImage::Default) {
64        EmptyImage::Default => rsx! {
65            svg {
66                class: "adui-empty-image-svg",
67                view_box: "0 0 64 41",
68                xmlns: "http://www.w3.org/2000/svg",
69                path { d: "M8 33h48v2H8z", fill: "#f5f5f5" }
70                rect { x: "16", y: "13", width: "32", height: "16", rx: "2", fill: "#fafafa", stroke: "#e5e5e5" }
71                circle { cx: "24", cy: "21", r: "3", fill: "#e5e5e5" }
72                rect { x: "30", y: "19", width: "12", height: "2", fill: "#e5e5e5" }
73                rect { x: "30", y: "23", width: "10", height: "2", fill: "#f0f0f0" }
74            }
75        },
76        EmptyImage::Simple => rsx! {
77            div { class: "adui-empty-image-simple" }
78        },
79        EmptyImage::Small => rsx! {
80            div { class: "adui-empty-image-simple" }
81        },
82        EmptyImage::Custom(url) => rsx! {
83            img { class: "adui-empty-image-img", src: "{url}", alt: "empty" }
84        },
85    };
86
87    rsx! {
88        div { class: "{class_attr}", style: "{style_attr}",
89            div { class: "adui-empty-image",
90                {image_node}
91            }
92            p { class: "adui-empty-description", "{description_text}" }
93            if let Some(footer_node) = footer {
94                div { class: "adui-empty-footer", {footer_node} }
95            }
96        }
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn empty_image_variants() {
106        assert!(EmptyImage::Default != EmptyImage::Simple);
107        assert!(EmptyImage::Simple != EmptyImage::Small);
108        assert!(EmptyImage::Default != EmptyImage::Small);
109    }
110
111    #[test]
112    fn empty_image_custom() {
113        let custom1 = EmptyImage::Custom("url1".to_string());
114        let custom2 = EmptyImage::Custom("url2".to_string());
115        let custom3 = EmptyImage::Custom("url1".to_string());
116
117        assert!(custom1 != custom2);
118        assert!(custom1 == custom3);
119    }
120
121    #[test]
122    fn empty_image_equality() {
123        assert!(EmptyImage::Default == EmptyImage::Default);
124        assert!(EmptyImage::Simple == EmptyImage::Simple);
125        assert!(EmptyImage::Small == EmptyImage::Small);
126    }
127
128    #[test]
129    fn empty_image_clone() {
130        let original = EmptyImage::Default;
131        let cloned = original.clone();
132        assert!(original == cloned);
133
134        let custom_original = EmptyImage::Custom("test".to_string());
135        let custom_cloned = custom_original.clone();
136        assert!(custom_original == custom_cloned);
137    }
138
139    #[test]
140    fn empty_props_defaults() {
141        let props = EmptyProps {
142            description: None,
143            image: None,
144            class: None,
145            style: None,
146            footer: None,
147        };
148        assert!(props.description.is_none());
149        assert!(props.image.is_none());
150    }
151
152    #[test]
153    fn empty_image_custom_string() {
154        let url = "https://example.com/image.png";
155        let custom = EmptyImage::Custom(url.to_string());
156        match custom {
157            EmptyImage::Custom(s) => assert_eq!(s, url),
158            _ => panic!("Expected Custom variant"),
159        }
160    }
161
162    #[test]
163    fn empty_image_all_variants() {
164        let default = EmptyImage::Default;
165        let simple = EmptyImage::Simple;
166        let small = EmptyImage::Small;
167        let custom = EmptyImage::Custom("test".to_string());
168
169        assert_ne!(default, simple);
170        assert_ne!(default, small);
171        assert_ne!(default, custom);
172        assert_ne!(simple, small);
173        assert_ne!(simple, custom);
174        assert_ne!(small, custom);
175    }
176
177    #[test]
178    fn empty_image_debug() {
179        let default = EmptyImage::Default;
180        let debug_str = format!("{:?}", default);
181        // Just verify it doesn't panic
182        assert!(!debug_str.is_empty());
183    }
184
185    #[test]
186    fn empty_props_with_all_fields() {
187        let props = EmptyProps {
188            description: Some("Custom description".to_string()),
189            image: Some(EmptyImage::Simple),
190            class: Some("custom-class".to_string()),
191            style: Some("color: red;".to_string()),
192            footer: None,
193        };
194        assert_eq!(props.description, Some("Custom description".to_string()));
195        assert_eq!(props.image, Some(EmptyImage::Simple));
196        assert_eq!(props.class, Some("custom-class".to_string()));
197    }
198
199    #[test]
200    fn empty_props_minimal() {
201        let props = EmptyProps {
202            description: None,
203            image: None,
204            class: None,
205            style: None,
206            footer: None,
207        };
208        assert!(props.description.is_none());
209        assert!(props.image.is_none());
210        assert!(props.class.is_none());
211        assert!(props.style.is_none());
212        assert!(props.footer.is_none());
213    }
214
215    #[test]
216    fn empty_image_custom_empty_string() {
217        let custom = EmptyImage::Custom(String::new());
218        match custom {
219            EmptyImage::Custom(s) => assert_eq!(s, ""),
220            _ => panic!("Expected Custom variant"),
221        }
222    }
223
224    #[test]
225    fn empty_image_custom_long_string() {
226        let long_url = "https://example.com/very/long/path/to/image.png?query=param&other=value";
227        let custom = EmptyImage::Custom(long_url.to_string());
228        match custom {
229            EmptyImage::Custom(s) => assert_eq!(s, long_url),
230            _ => panic!("Expected Custom variant"),
231        }
232    }
233
234    #[test]
235    fn empty_props_clone() {
236        let props = EmptyProps {
237            description: Some("Test".to_string()),
238            image: Some(EmptyImage::Default),
239            class: None,
240            style: None,
241            footer: None,
242        };
243        let cloned = props.clone();
244        assert_eq!(props.description, cloned.description);
245        assert_eq!(props.image, cloned.image);
246    }
247
248    #[test]
249    fn empty_image_custom_equality_with_same_string() {
250        let custom1 = EmptyImage::Custom("test".to_string());
251        let custom2 = EmptyImage::Custom("test".to_string());
252        assert_eq!(custom1, custom2);
253    }
254
255    #[test]
256    fn empty_image_custom_equality_with_different_string() {
257        let custom1 = EmptyImage::Custom("test1".to_string());
258        let custom2 = EmptyImage::Custom("test2".to_string());
259        assert_ne!(custom1, custom2);
260    }
261}