yggdrasil/pages/admin/settings/
backup_section.rs1use 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#[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 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 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 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 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 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}