1use crate::components::button::{Button, ButtonType};
10use crate::components::overlay::{OverlayKey, OverlayKind, use_overlay};
11use crate::foundation::{
12 ClassListExt, ModalClassNames, ModalSemantic, ModalStyles, StyleStringExt,
13};
14use dioxus::events::KeyboardEvent;
15use dioxus::prelude::*;
16use std::collections::HashMap;
17use std::rc::Rc;
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub enum ModalType {
22 Info,
23 Success,
24 Error,
25 Warning,
26 Confirm,
27}
28
29impl ModalType {
30 fn as_class(&self) -> &'static str {
31 match self {
32 ModalType::Info => "adui-modal-info",
33 ModalType::Success => "adui-modal-success",
34 ModalType::Error => "adui-modal-error",
35 ModalType::Warning => "adui-modal-warning",
36 ModalType::Confirm => "adui-modal-confirm",
37 }
38 }
39}
40
41#[derive(Props, Clone)]
43pub struct ModalProps {
44 pub open: bool,
46 #[props(optional)]
48 pub title: Option<String>,
49 #[props(optional)]
52 pub footer: Option<Element>,
53 #[props(optional)]
56 pub footer_render: Option<Rc<dyn Fn(Element, FooterExtra) -> Element>>,
57 #[props(default = true)]
59 pub show_footer: bool,
60 #[props(optional)]
62 pub on_ok: Option<EventHandler<()>>,
63 #[props(optional)]
65 pub on_cancel: Option<EventHandler<()>>,
66 #[props(default = true)]
69 pub closable: bool,
70 #[props(optional)]
72 pub closable_config: Option<ClosableConfig>,
73 #[props(default = true)]
75 pub mask_closable: bool,
76 #[props(default)]
78 pub destroy_on_close: bool,
79 #[props(default)]
81 pub destroy_on_hidden: bool,
82 #[props(default)]
84 pub force_render: bool,
85 #[props(optional)]
88 pub width: Option<f32>,
89 #[props(optional)]
91 pub width_responsive: Option<HashMap<String, f32>>,
92 #[props(default)]
94 pub centered: bool,
95 #[props(default)]
97 pub confirm_loading: bool,
98 #[props(optional)]
100 pub ok_text: Option<String>,
101 #[props(optional)]
103 pub cancel_text: Option<String>,
104 #[props(optional)]
106 pub ok_type: Option<ButtonType>,
107 #[props(default = true)]
109 pub keyboard: bool,
110 #[props(optional)]
112 pub close_icon: Option<Element>,
113 #[props(optional)]
115 pub after_close: Option<EventHandler<()>>,
116 #[props(optional)]
118 pub after_open_change: Option<EventHandler<bool>>,
119 #[props(optional)]
121 pub class: Option<String>,
122 #[props(optional)]
124 pub style: Option<String>,
125 #[props(optional)]
127 pub class_names: Option<ModalClassNames>,
128 #[props(optional)]
130 pub styles: Option<ModalStyles>,
131 #[props(optional)]
133 pub get_container: Option<String>,
134 #[props(optional)]
136 pub z_index: Option<i32>,
137 #[props(optional)]
139 pub mask: Option<MaskConfig>,
140 #[props(optional)]
142 pub modal_render: Option<Rc<dyn Fn(Element) -> Element>>,
143 #[props(optional)]
145 pub mouse_position: Option<(f32, f32)>,
146 #[props(default)]
148 pub loading: bool,
149 #[props(optional)]
151 pub ok_button_props: Option<HashMap<String, String>>,
152 #[props(optional)]
154 pub cancel_button_props: Option<HashMap<String, String>>,
155 pub children: Element,
156}
157
158impl PartialEq for ModalProps {
159 fn eq(&self, other: &Self) -> bool {
160 self.open == other.open
162 && self.title == other.title
163 && self.footer == other.footer
164 && self.show_footer == other.show_footer
165 && self.closable == other.closable
166 && self.mask_closable == other.mask_closable
167 && self.destroy_on_close == other.destroy_on_close
168 && self.destroy_on_hidden == other.destroy_on_hidden
169 && self.force_render == other.force_render
170 && self.width == other.width
171 && self.width_responsive == other.width_responsive
172 && self.centered == other.centered
173 && self.confirm_loading == other.confirm_loading
174 && self.ok_text == other.ok_text
175 && self.cancel_text == other.cancel_text
176 && self.ok_type == other.ok_type
177 && self.keyboard == other.keyboard
178 && self.close_icon == other.close_icon
179 && self.after_close == other.after_close
180 && self.after_open_change == other.after_open_change
181 && self.class == other.class
182 && self.style == other.style
183 && self.class_names == other.class_names
184 && self.styles == other.styles
185 && self.get_container == other.get_container
186 && self.z_index == other.z_index
187 && self.mask == other.mask
188 && self.mouse_position == other.mouse_position
189 && self.loading == other.loading
190 && self.ok_button_props == other.ok_button_props
191 && self.cancel_button_props == other.cancel_button_props
192 && self.closable_config == other.closable_config
193 }
195}
196
197#[derive(Clone, Debug)]
199pub struct FooterExtra {
200 pub ok_btn: Element,
202 pub cancel_btn: Element,
204}
205
206#[derive(Clone)]
208pub struct ClosableConfig {
209 pub show: bool,
211 pub on_close: Option<Rc<dyn Fn()>>,
213 pub after_close: Option<Rc<dyn Fn()>>,
215}
216
217impl PartialEq for ClosableConfig {
218 fn eq(&self, other: &Self) -> bool {
219 self.show == other.show
220 }
222}
223
224impl std::fmt::Debug for ClosableConfig {
225 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226 f.debug_struct("ClosableConfig")
227 .field("show", &self.show)
228 .field("on_close", &"<function>")
229 .field("after_close", &"<function>")
230 .finish()
231 }
232}
233
234#[derive(Clone, Debug, PartialEq)]
236pub struct MaskConfig {
237 pub visible: bool,
239 pub closable: bool,
241 pub style: Option<String>,
243}
244
245#[component]
247pub fn Modal(props: ModalProps) -> Element {
248 let ModalProps {
249 open,
250 title,
251 footer,
252 show_footer,
253 on_ok,
254 on_cancel,
255 closable,
256 mask_closable,
257 destroy_on_close,
258 width,
259 centered,
260 confirm_loading,
261 ok_text,
262 cancel_text,
263 ok_type,
264 keyboard,
265 close_icon,
266 after_close,
267 after_open_change,
268 class,
269 style,
270 class_names,
271 styles,
272 children,
273 ..
274 } = props;
275
276 let prev_open: Signal<bool> = use_signal(|| open);
278
279 let overlay = use_overlay();
282 let modal_key: Signal<Option<OverlayKey>> = use_signal(|| None);
283 let z_index: Signal<i32> = use_signal(|| 1000);
284
285 {
286 let overlay = overlay.clone();
287 let mut key_signal = modal_key;
288 let mut z_signal = z_index;
289 let mut prev_signal = prev_open;
290 use_effect(move || {
291 if let Some(handle) = overlay.clone() {
292 let current_key = {
293 let guard = key_signal.read();
294 *guard
295 };
296 if open {
297 if current_key.is_none() {
298 let (key, meta) = handle.open(OverlayKind::Modal, true);
299 z_signal.set(meta.z_index);
300 key_signal.set(Some(key));
301 }
302 } else if let Some(key) = current_key {
303 handle.close(key);
304 key_signal.set(None);
305 }
306 }
307
308 let prev = *prev_signal.read();
310 if prev != open {
311 if let Some(cb) = after_open_change {
312 cb.call(open);
313 }
314 if !open {
316 if let Some(cb) = after_close {
317 cb.call(());
318 }
319 }
320 prev_signal.set(open);
321 }
322 });
323 }
324
325 if !open && destroy_on_close {
326 return rsx! {};
327 }
328
329 let current_z = *z_index.read();
330 let width_px = width.unwrap_or(520.0);
331
332 let mut class_list = vec!["adui-modal".to_string()];
334 if centered {
335 class_list.push("adui-modal-centered".into());
336 }
337 class_list.push_semantic(&class_names, ModalSemantic::Root);
338 if let Some(extra) = class {
339 class_list.push(extra);
340 }
341 let class_attr = class_list
342 .into_iter()
343 .filter(|s| !s.is_empty())
344 .collect::<Vec<_>>()
345 .join(" ");
346
347 let mut style_attr = style.unwrap_or_default();
348 style_attr.append_semantic(&styles, ModalSemantic::Root);
349
350 let ok_handler = on_ok;
352 let cancel_handler = on_cancel;
353
354 let on_close = move || {
355 if let Some(cb) = cancel_handler {
356 cb.call(());
357 }
358 };
359
360 let handle_ok = move || {
361 if let Some(cb) = ok_handler {
362 cb.call(());
363 }
364 };
365
366 let on_keydown = move |evt: KeyboardEvent| {
367 if keyboard && matches!(evt.key(), Key::Escape) {
368 evt.prevent_default();
369 on_close();
370 }
371 };
372
373 let ok_button_text = ok_text.unwrap_or_else(|| "确定".to_string());
375 let cancel_button_text = cancel_text.unwrap_or_else(|| "取消".to_string());
376 let ok_button_type = ok_type.unwrap_or(ButtonType::Primary);
377
378 let close_icon_element = close_icon.unwrap_or_else(|| {
380 rsx! { "×" }
381 });
382
383 let content_style = if centered {
385 format!(
386 "position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; z-index: {}; {}",
387 current_z + 1,
388 style_attr
389 )
390 } else {
391 format!(
392 "position: fixed; top: 100px; left: 50%; transform: translateX(-50%); z-index: {}; {}",
393 current_z + 1,
394 style_attr
395 )
396 };
397
398 rsx! {
399 if open {
400 div {
402 class: "adui-modal-mask",
403 style: "position: fixed; inset: 0; background: rgba(0,0,0,0.45); z-index: {current_z};",
404 onclick: move |_| {
405 if mask_closable {
406 on_close();
407 }
408 }
409 }
410 div {
412 class: "{class_attr}",
413 style: "{content_style}",
414 onkeydown: on_keydown,
415 tabindex: 0,
416 div {
417 class: "adui-modal-content",
418 style: "min-width: {width_px}px; max-width: 80vw; background: var(--adui-color-bg-container); border-radius: var(--adui-radius-lg, 8px); box-shadow: var(--adui-shadow-secondary); border: 1px solid var(--adui-color-border); overflow: hidden;",
419 onclick: move |evt| evt.stop_propagation(),
420 if title.is_some() || closable {
422 div {
423 class: "adui-modal-header",
424 style: "display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; border-bottom: 1px solid var(--adui-color-border);",
425 if let Some(text) = title {
426 div { class: "adui-modal-title", "{text}" }
427 }
428 if closable {
429 button {
430 class: "adui-modal-close",
431 r#type: "button",
432 style: "border: none; background: none; cursor: pointer; font-size: 16px;",
433 onclick: move |_| on_close(),
434 {close_icon_element}
435 }
436 }
437 }
438 }
439 div {
441 class: "adui-modal-body",
442 style: "padding: 16px;",
443 {children}
444 }
445 if show_footer {
447 if let Some(footer_node) = footer {
448 div {
449 class: "adui-modal-footer",
450 style: "padding: 10px 16px; border-top: 1px solid var(--adui-color-border); text-align: right;",
451 {footer_node}
452 }
453 } else {
454 div {
455 class: "adui-modal-footer",
456 style: "padding: 10px 16px; border-top: 1px solid var(--adui-color-border); text-align: right; display: flex; gap: 8px; justify-content: flex-end;",
457 Button {
458 onclick: move |_| on_close(),
459 "{cancel_button_text}"
460 }
461 Button {
462 r#type: ok_button_type,
463 loading: confirm_loading,
464 onclick: move |_| handle_ok(),
465 "{ok_button_text}"
466 }
467 }
468 }
469 }
470 }
471 }
472 }
473 }
474}
475
476#[cfg(test)]
477mod tests {
478 use super::*;
479
480 #[test]
481 fn modal_type_classes() {
482 assert_eq!(ModalType::Info.as_class(), "adui-modal-info");
483 assert_eq!(ModalType::Success.as_class(), "adui-modal-success");
484 assert_eq!(ModalType::Error.as_class(), "adui-modal-error");
485 }
486}