adui_dioxus/components/
list.rs

1use crate::components::config_provider::ComponentSize;
2use crate::components::empty::Empty;
3use crate::components::pagination::Pagination;
4use crate::components::spin::Spin;
5use dioxus::prelude::*;
6
7/// Props for the List component (MVP subset).
8#[derive(Props, Clone, PartialEq)]
9pub struct ListProps {
10    /// Optional header content displayed above the list items.
11    #[props(optional)]
12    pub header: Option<Element>,
13    /// Optional footer content displayed below the list body.
14    #[props(optional)]
15    pub footer: Option<Element>,
16    /// Whether to render a border around the list.
17    #[props(default)]
18    pub bordered: bool,
19    /// Visual density. When set, maps to size-specific classes.
20    #[props(optional)]
21    pub size: Option<ComponentSize>,
22    /// Extra class name for the root element.
23    #[props(optional)]
24    pub class: Option<String>,
25    /// Inline style for the root element.
26    #[props(optional)]
27    pub style: Option<String>,
28    /// Whether the list is in loading state. When true, the body is wrapped
29    /// with a Spin overlay.
30    #[props(default)]
31    pub loading: bool,
32    /// Whether the data set is empty (used together with `loading=false`).
33    #[props(optional)]
34    pub is_empty: Option<bool>,
35    /// Custom empty content. When omitted and `is_empty = Some(true)`, the
36    /// built-in `Empty` component is used.
37    #[props(optional)]
38    pub empty: Option<Element>,
39    /// Pagination total items. When set, enables a simple Pagination control
40    /// at the bottom of the list.
41    #[props(optional)]
42    pub pagination_total: Option<u32>,
43    /// Current page index for pagination (1-based).
44    #[props(optional)]
45    pub pagination_current: Option<u32>,
46    /// Page size for pagination.
47    #[props(optional)]
48    pub pagination_page_size: Option<u32>,
49    /// Callback when pagination changes.
50    #[props(optional)]
51    pub pagination_on_change: Option<EventHandler<(u32, u32)>>,
52    /// List item content. Callers通常在内部渲染多个块元素,并使用
53    /// `.adui-list-item` 类名标记每一行。
54    pub children: Element,
55}
56
57/// Ant Design flavored List component (MVP).
58#[component]
59pub fn List(props: ListProps) -> Element {
60    let ListProps {
61        header,
62        footer,
63        bordered,
64        size,
65        class,
66        style,
67        loading,
68        is_empty,
69        empty,
70        pagination_total,
71        pagination_current,
72        pagination_page_size,
73        pagination_on_change,
74        children,
75    } = props;
76
77    let mut class_list = vec!["adui-list".to_string()];
78    if bordered {
79        class_list.push("adui-list-bordered".into());
80    }
81    if let Some(sz) = size {
82        match sz {
83            ComponentSize::Small => class_list.push("adui-list-sm".into()),
84            ComponentSize::Middle => {}
85            ComponentSize::Large => class_list.push("adui-list-lg".into()),
86        }
87    }
88    if let Some(extra) = class {
89        class_list.push(extra);
90    }
91    let class_attr = class_list.join(" ");
92    let style_attr = style.unwrap_or_default();
93
94    let show_empty = !loading && is_empty.unwrap_or(false);
95
96    rsx! {
97        div { class: "{class_attr}", style: "{style_attr}",
98            if let Some(head) = header {
99                div { class: "adui-list-header", {head} }
100            }
101
102            // Body
103            if loading {
104                div { class: "adui-list-body",
105                    Spin {
106                        spinning: Some(true),
107                        tip: Some("加载中...".to_string()),
108                        div { class: "adui-list-items", {children} }
109                    }
110                }
111            } else if show_empty {
112                div { class: "adui-list-body",
113                    div { class: "adui-list-empty",
114                        if let Some(node) = empty {
115                            {node}
116                        } else {
117                            Empty {}
118                        }
119                    }
120                }
121            } else {
122                div { class: "adui-list-body",
123                    div { class: "adui-list-items", {children} }
124                }
125            }
126
127            if let Some(foot) = footer {
128                div { class: "adui-list-footer", {foot} }
129            }
130
131            // Pagination
132            if let Some(total) = pagination_total {
133                div { class: "adui-list-pagination",
134                    Pagination {
135                        total: total,
136                        current: pagination_current,
137                        page_size: pagination_page_size,
138                        show_total: false,
139                        show_size_changer: false,
140                        on_change: move |(page, size)| {
141                            if let Some(cb) = pagination_on_change {
142                                cb.call((page, size));
143                            }
144                        },
145                    }
146                }
147            }
148        }
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[test]
157    fn list_props_defaults() {
158        let props = ListProps {
159            header: None,
160            footer: None,
161            bordered: false,
162            size: None,
163            class: None,
164            style: None,
165            loading: false,
166            is_empty: None,
167            empty: None,
168            pagination_total: None,
169            pagination_current: None,
170            pagination_page_size: None,
171            pagination_on_change: None,
172            children: rsx!(div {}),
173        };
174        assert_eq!(props.bordered, false);
175        assert_eq!(props.loading, false);
176        assert!(props.header.is_none());
177        assert!(props.footer.is_none());
178    }
179
180    #[test]
181    fn list_props_bordered() {
182        let props = ListProps {
183            header: None,
184            footer: None,
185            bordered: true,
186            size: None,
187            class: None,
188            style: None,
189            loading: false,
190            is_empty: None,
191            empty: None,
192            pagination_total: None,
193            pagination_current: None,
194            pagination_page_size: None,
195            pagination_on_change: None,
196            children: rsx!(div {}),
197        };
198        assert_eq!(props.bordered, true);
199    }
200
201    #[test]
202    fn list_props_loading() {
203        let props = ListProps {
204            header: None,
205            footer: None,
206            bordered: false,
207            size: None,
208            class: None,
209            style: None,
210            loading: true,
211            is_empty: None,
212            empty: None,
213            pagination_total: None,
214            pagination_current: None,
215            pagination_page_size: None,
216            pagination_on_change: None,
217            children: rsx!(div {}),
218        };
219        assert_eq!(props.loading, true);
220    }
221
222    #[test]
223    fn list_props_size() {
224        let props = ListProps {
225            header: None,
226            footer: None,
227            bordered: false,
228            size: Some(ComponentSize::Small),
229            class: None,
230            style: None,
231            loading: false,
232            is_empty: None,
233            empty: None,
234            pagination_total: None,
235            pagination_current: None,
236            pagination_page_size: None,
237            pagination_on_change: None,
238            children: rsx!(div {}),
239        };
240        assert_eq!(props.size, Some(ComponentSize::Small));
241    }
242
243    #[test]
244    fn list_props_pagination() {
245        let props = ListProps {
246            header: None,
247            footer: None,
248            bordered: false,
249            size: None,
250            class: None,
251            style: None,
252            loading: false,
253            is_empty: None,
254            empty: None,
255            pagination_total: Some(100),
256            pagination_current: Some(1),
257            pagination_page_size: Some(10),
258            pagination_on_change: None,
259            children: rsx!(div {}),
260        };
261        assert_eq!(props.pagination_total, Some(100));
262        assert_eq!(props.pagination_current, Some(1));
263        assert_eq!(props.pagination_page_size, Some(10));
264    }
265}