Skip to main content

yggdrasil/pages/admin/settings/
backup_section.rs

1//! 自动备份调度分区(调度开关、执行时间、保留份数、是否含 uploads)。
2//!
3//! 从 /admin/system 的备份设置卡片整合到设置页面。备份操作(创建/恢复/删除)
4//! 仍在 /admin/system 的备份 tab。写入后唤醒调度任务立即重排。
5
6use dioxus::prelude::*;
7
8#[cfg(target_arch = "wasm32")]
9use crate::api::settings::{get_backup_settings, update_backup_settings};
10#[cfg(target_arch = "wasm32")]
11use crate::components::forms::{FormLabel, TimePicker, INPUT_CLASS};
12#[cfg(target_arch = "wasm32")]
13use crate::components::ui::{Checkbox, LoadingButton, ADMIN_CARD_CLASS};
14#[cfg(target_arch = "wasm32")]
15use crate::models::settings::BackupSettings;
16#[cfg(target_arch = "wasm32")]
17use crate::utils::time::{local_hhmm_to_utc, utc_hhmm_to_local};
18
19/// 自动备份调度分区组件。
20#[allow(non_snake_case)]
21#[component]
22pub fn BackupSection(toast: Callback<(String, bool)>) -> Element {
23    #[cfg(target_arch = "wasm32")]
24    {
25        let mut saved: Signal<BackupSettings> = use_signal(BackupSettings::default);
26        let mut auto_draft: Signal<bool> = use_signal(|| false);
27        let mut time_draft: Signal<String> = use_signal(|| "04:00".to_string());
28        let mut retention_draft: Signal<i32> = use_signal(|| 30);
29        let mut include_draft: Signal<bool> = use_signal(|| true);
30        let loading = use_signal(|| true);
31        let mut saving = use_signal(|| false);
32        let mut just_saved = use_signal(|| false);
33
34        use_effect(move || {
35            #[cfg(target_arch = "wasm32")]
36            {
37                let mut saved = saved;
38                let mut auto_draft = auto_draft;
39                let mut time_draft = time_draft;
40                let mut retention_draft = retention_draft;
41                let mut include_draft = include_draft;
42                let mut loading = loading;
43                spawn(async move {
44                    match get_backup_settings().await {
45                        Ok(v) => {
46                            let s = v.settings;
47                            saved.set(s.clone());
48                            auto_draft.set(s.auto_enabled);
49                            time_draft.set(utc_hhmm_to_local(&s.time_utc));
50                            retention_draft.set(s.retention_count);
51                            include_draft.set(s.include_uploads);
52                        }
53                        Err(e) => toast.call((format!("加载失败:{e}"), true)),
54                    }
55                    loading.set(false);
56                });
57            }
58        });
59
60        let dirty = use_memo(move || {
61            let s = saved();
62            auto_draft() != s.auto_enabled
63                || time_draft() != utc_hhmm_to_local(&s.time_utc)
64                || retention_draft() != s.retention_count
65                || include_draft() != s.include_uploads
66        });
67
68        rsx! {
69            div { class: "space-y-6",
70                div { class: "{ADMIN_CARD_CLASS} p-6 md:p-8 flex flex-col gap-6",
71                    div { class: "flex items-center gap-3",
72                        span { class: "inline-flex items-center justify-center w-10 h-10 rounded-full bg-[var(--color-paper-theme)] text-[var(--color-paper-primary)] border border-[var(--color-paper-border)]",
73                            svg {
74                                class: "w-5 h-5",
75                                view_box: "0 0 24 24",
76                                fill: "none",
77                                stroke: "currentColor",
78                                stroke_width: "1.8",
79                                stroke_linecap: "round",
80                                stroke_linejoin: "round",
81                                path { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }
82                                polyline { points: "17 8 12 3 7 8" }
83                                line {
84                                    x1: "12",
85                                    y1: "3",
86                                    x2: "12",
87                                    y2: "15",
88                                }
89                            }
90                        }
91                        div {
92                            h2 { class: "text-xl font-bold text-[var(--color-paper-primary)]",
93                                "自动备份"
94                            }
95                            p { class: "text-sm text-[var(--color-paper-secondary)] mt-0.5",
96                                "定时自动备份调度策略。备份操作(创建/恢复/删除)在「系统」页面。"
97                            }
98                        }
99                    }
100
101                    // 启用开关
102                    label { class: "flex items-center gap-3 cursor-pointer max-w-xl",
103                        Checkbox {
104                            checked: auto_draft(),
105                            onchange: move |checked: bool| {
106                                auto_draft.set(checked);
107                                just_saved.set(false);
108                            },
109                        }
110                        div { class: "flex flex-col",
111                            span { class: "text-sm font-medium text-[var(--color-paper-primary)]",
112                                "启用每天定时备份"
113                            }
114                            span { class: "text-xs text-[var(--color-paper-secondary)]",
115                                "调度任务按面板写入的 DB 值运行,保存后立即重排。"
116                            }
117                        }
118                    }
119
120                    // 执行时间(本地时间显示/编辑,存 UTC)
121                    div { class: "flex flex-col gap-2 max-w-xl",
122                        FormLabel {
123                            label: "执行时间",
124                            html_for: Some("backup-time".to_string()),
125                        }
126                        p { class: "text-xs text-[var(--color-paper-secondary)]",
127                            "按浏览器本地时间显示与编辑,服务端以 UTC 存储"
128                        }
129                        div { class: "flex items-center gap-3",
130                            TimePicker {
131                                id: Some("backup-time".to_string()),
132                                value: time_draft(),
133                                onchange: move |v: String| {
134                                    time_draft.set(v);
135                                    just_saved.set(false);
136                                },
137                            }
138                            span { class: "text-xs text-[var(--color-paper-secondary)]",
139                                "本地时间"
140                            }
141                        }
142                    }
143
144                    // 保留份数
145                    div { class: "flex flex-col gap-2 max-w-xl",
146                        FormLabel {
147                            label: "自动备份保留份数",
148                            html_for: Some("backup-retention".to_string()),
149                        }
150                        input {
151                            id: "backup-retention",
152                            r#type: "number",
153                            min: "1",
154                            max: "365",
155                            class: "{INPUT_CLASS}",
156                            value: "{retention_draft()}",
157                            oninput: move |e: Event<FormData>| {
158                                let v = e.value();
159                                if let Ok(n) = v.parse::<i32>() {
160                                    retention_draft.set(n);
161                                }
162                                just_saved.set(false);
163                            },
164                        }
165                        p { class: "text-xs text-[var(--color-paper-secondary)]",
166                            "超出后最旧的备份连配对 uploads 包一起删除(1–365)。手动备份永不自动删除。"
167                        }
168                    }
169
170                    // 含 uploads
171                    label { class: "flex items-center gap-3 cursor-pointer max-w-xl",
172                        Checkbox {
173                            checked: include_draft(),
174                            onchange: move |checked: bool| {
175                                include_draft.set(checked);
176                                just_saved.set(false);
177                            },
178                        }
179                        div { class: "flex flex-col",
180                            span { class: "text-sm font-medium text-[var(--color-paper-primary)]",
181                                "备份包含 uploads 素材"
182                            }
183                            span { class: "text-xs text-[var(--color-paper-secondary)]",
184                                "打包 tar.gz(排除可重建的 .cache)。"
185                            }
186                        }
187                    }
188
189                    // 操作行
190                    div { class: "flex items-center justify-between gap-4 pt-1",
191                        if just_saved() {
192                            span { class: "inline-flex items-center gap-1.5 text-xs text-[var(--color-paper-accent)]",
193                                svg {
194                                    class: "w-3.5 h-3.5",
195                                    view_box: "0 0 24 24",
196                                    fill: "none",
197                                    stroke: "currentColor",
198                                    stroke_width: "2.5",
199                                    path {
200                                        stroke_linecap: "round",
201                                        stroke_linejoin: "round",
202                                        d: "M5 13l4 4L19 7",
203                                    }
204                                }
205                                "已保存"
206                            }
207                        } else if dirty() {
208                            span { class: "text-xs text-[var(--color-paper-secondary)]",
209                                "有未保存的更改"
210                            }
211                        } else {
212                            span { class: "text-xs text-transparent select-none", "·" }
213                        }
214                        LoadingButton {
215                            label: "保存设置".to_string(),
216                            loading: saving(),
217                            disabled: loading() || just_saved() || !dirty(),
218                            onclick: move |_| {
219                                let a = auto_draft();
220                                let t = local_hhmm_to_utc(&time_draft());
221                                let r = retention_draft();
222                                let i = include_draft();
223                                saving.set(true);
224                                spawn(async move {
225                                    match update_backup_settings(a, t, r, i).await {
226                                        Ok(v) => {
227                                            let s = v.settings;
228                                            saved.set(s.clone());
229                                            auto_draft.set(s.auto_enabled);
230                                            time_draft.set(utc_hhmm_to_local(&s.time_utc));
231                                            retention_draft.set(s.retention_count);
232                                            include_draft.set(s.include_uploads);
233                                            just_saved.set(true);
234                                            toast.call(("保存成功".to_string(), false));
235                                        }
236                                        Err(e) => toast.call((format!("保存失败:{e}"), true)),
237                                    }
238                                    saving.set(false);
239                                });
240                            },
241                        }
242                    }
243                }
244            }
245        }
246    }
247    #[cfg(not(target_arch = "wasm32"))]
248    {
249        rsx! {
250            div { class: "p-8 text-[var(--color-paper-secondary)]", "备份配置(前端渲染)" }
251        }
252    }
253}