Skip to main content

yggdrasil/pages/admin/settings/
image_section.rs

1//! 图片处理配置分区(WebP 编码 + 尺寸上限 + 尺寸缓存 TTL)。
2//!
3//! Tier B 可编辑+重启生效。
4
5use dioxus::prelude::*;
6
7#[cfg(target_arch = "wasm32")]
8use crate::api::settings::{
9    get_image_limit_settings, get_webp_settings, update_image_limit_settings, update_webp_settings,
10};
11#[cfg(target_arch = "wasm32")]
12use crate::components::forms::{FormLabel, INPUT_CLASS};
13#[cfg(target_arch = "wasm32")]
14use crate::components::ui::{LoadingButton, ADMIN_CARD_CLASS};
15#[cfg(target_arch = "wasm32")]
16use crate::models::settings::{ImageLimitSettings, WebpSettings};
17
18/// 图片处理配置分区组件。
19#[allow(non_snake_case)]
20#[component]
21pub fn ImageSection(toast: Callback<(String, bool)>) -> Element {
22    #[cfg(target_arch = "wasm32")]
23    {
24        // WebP 编码
25        let mut webp_saved: Signal<WebpSettings> = use_signal(WebpSettings::default);
26        let mut webp_draft: Signal<WebpSettings> = use_signal(WebpSettings::default);
27        let webp_loading = use_signal(|| true);
28        let mut webp_saving = use_signal(|| false);
29        let mut webp_just_saved = use_signal(|| false);
30
31        // 图片限制
32        let mut img_saved: Signal<ImageLimitSettings> = use_signal(ImageLimitSettings::default);
33        let mut img_draft: Signal<ImageLimitSettings> = use_signal(ImageLimitSettings::default);
34        let img_loading = use_signal(|| true);
35        let mut img_saving = use_signal(|| false);
36        let mut img_just_saved = use_signal(|| false);
37
38        use_effect(move || {
39            #[cfg(target_arch = "wasm32")]
40            {
41                let mut webp_saved = webp_saved;
42                let mut webp_draft = webp_draft;
43                let mut webp_loading = webp_loading;
44                let mut img_saved = img_saved;
45                let mut img_draft = img_draft;
46                let mut img_loading = img_loading;
47                spawn(async move {
48                    match get_webp_settings().await {
49                        Ok(s) => {
50                            webp_saved.set(s.clone());
51                            webp_draft.set(s);
52                        }
53                        Err(e) => toast.call((format!("WebP 加载失败:{e}"), true)),
54                    }
55                    match get_image_limit_settings().await {
56                        Ok(s) => {
57                            img_saved.set(s.clone());
58                            img_draft.set(s);
59                        }
60                        Err(e) => toast.call((format!("图片限制加载失败:{e}"), true)),
61                    }
62                    webp_loading.set(false);
63                    img_loading.set(false);
64                });
65            }
66        });
67
68        let webp_dirty = use_memo(move || webp_draft() != webp_saved());
69        let img_dirty = use_memo(move || img_draft() != img_saved());
70
71        rsx! {
72            div { class: "space-y-6",
73                // WebP 编码卡片
74                div { class: "{ADMIN_CARD_CLASS} p-6 md:p-8 flex flex-col gap-6",
75                    div { class: "flex items-center gap-3",
76                        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)]",
77                            svg {
78                                class: "w-5 h-5",
79                                view_box: "0 0 24 24",
80                                fill: "none",
81                                stroke: "currentColor",
82                                stroke_width: "1.8",
83                                stroke_linecap: "round",
84                                stroke_linejoin: "round",
85                                path { d: "M21 15l-5-5L5 21 M18 21H3V3h18v9z" }
86                                circle { cx: "7", cy: "7", r: "1.5" }
87                            }
88                        }
89                        div {
90                            h2 { class: "text-xl font-bold text-[var(--color-paper-primary)]",
91                                "WebP 编码"
92                            }
93                            p { class: "text-sm text-[var(--color-paper-secondary)] mt-0.5",
94                                span { class: "text-amber-600 dark:text-amber-400 font-medium",
95                                    "⚠ 修改后需重启服务生效。"
96                                }
97                            }
98                        }
99                    }
100
101                    div { class: "flex flex-col gap-2 max-w-xl",
102                        FormLabel {
103                            label: "质量系数(0.0–100.0)",
104                            html_for: Some("webp-quality".to_string()),
105                        }
106                        input {
107                            id: "webp-quality",
108                            r#type: "number",
109                            min: "0",
110                            max: "100",
111                            step: "1",
112                            class: "{INPUT_CLASS}",
113                            value: "{webp_draft().quality}",
114                            oninput: move |e: Event<FormData>| {
115                                let v = e.value();
116                                if let Ok(n) = v.parse::<f32>() {
117                                    let mut d = webp_draft();
118                                    d.quality = n;
119                                    webp_draft.set(d);
120                                    webp_just_saved.set(false);
121                                }
122                            },
123                        }
124                        p { class: "text-xs text-[var(--color-paper-secondary)]",
125                            "0=最小体积,100=最佳质量。默认 85。"
126                        }
127                    }
128
129                    div { class: "flex flex-col gap-2 max-w-xl",
130                        FormLabel {
131                            label: "编码方法(0–6)",
132                            html_for: Some("webp-method".to_string()),
133                        }
134                        input {
135                            id: "webp-method",
136                            r#type: "number",
137                            min: "0",
138                            max: "6",
139                            class: "{INPUT_CLASS}",
140                            value: "{webp_draft().method}",
141                            oninput: move |e: Event<FormData>| {
142                                let v = e.value();
143                                if let Ok(n) = v.parse::<u32>() {
144                                    let mut d = webp_draft();
145                                    d.method = n;
146                                    webp_draft.set(d);
147                                    webp_just_saved.set(false);
148                                }
149                            },
150                        }
151                        p { class: "text-xs text-[var(--color-paper-secondary)]",
152                            "数值越大压缩率越高但越慢。默认 2。"
153                        }
154                    }
155
156                    div { class: "flex items-center justify-between gap-4 pt-1",
157                        if webp_just_saved() {
158                            span { class: "inline-flex items-center gap-1.5 text-xs text-[var(--color-paper-accent)]",
159                                svg {
160                                    class: "w-3.5 h-3.5",
161                                    view_box: "0 0 24 24",
162                                    fill: "none",
163                                    stroke: "currentColor",
164                                    stroke_width: "2.5",
165                                    path {
166                                        stroke_linecap: "round",
167                                        stroke_linejoin: "round",
168                                        d: "M5 13l4 4L19 7",
169                                    }
170                                }
171                                "已保存(重启后生效)"
172                            }
173                        } else if webp_dirty() {
174                            span { class: "text-xs text-[var(--color-paper-secondary)]",
175                                "有未保存的更改"
176                            }
177                        } else {
178                            span { class: "text-xs text-transparent select-none", "·" }
179                        }
180                        LoadingButton {
181                            label: "保存设置".to_string(),
182                            loading: webp_saving(),
183                            disabled: webp_loading() || webp_just_saved() || !webp_dirty(),
184                            onclick: move |_| {
185                                let d = webp_draft();
186                                webp_saving.set(true);
187                                spawn(async move {
188                                    match update_webp_settings(d.quality, d.method).await {
189                                        Ok(s) => {
190                                            webp_saved.set(s.clone());
191                                            webp_draft.set(s);
192                                            webp_just_saved.set(true);
193                                            toast.call(("保存成功".to_string(), false));
194                                        }
195                                        Err(e) => toast.call((format!("保存失败:{e}"), true)),
196                                    }
197                                    webp_saving.set(false);
198                                });
199                            },
200                        }
201                    }
202                }
203
204                // 图片尺寸限制卡片
205                div { class: "{ADMIN_CARD_CLASS} p-6 md:p-8 flex flex-col gap-6",
206                    div {
207                        h2 { class: "text-xl font-bold text-[var(--color-paper-primary)]",
208                            "图片尺寸限制"
209                        }
210                        p { class: "text-sm text-[var(--color-paper-secondary)] mt-0.5",
211                            span { class: "text-amber-600 dark:text-amber-400 font-medium",
212                                "⚠ 修改后需重启服务生效。"
213                            }
214                        }
215                    }
216
217                    div { class: "flex flex-col gap-2 max-w-xl",
218                        FormLabel {
219                            label: "单边最大尺寸(像素)",
220                            html_for: Some("img-dim".to_string()),
221                        }
222                        input {
223                            id: "img-dim",
224                            r#type: "number",
225                            min: "512",
226                            class: "{INPUT_CLASS}",
227                            value: "{img_draft().max_dimension}",
228                            oninput: move |e: Event<FormData>| {
229                                let v = e.value();
230                                if let Ok(n) = v.parse::<u32>() {
231                                    let mut d = img_draft();
232                                    d.max_dimension = n;
233                                    img_draft.set(d);
234                                    img_just_saved.set(false);
235                                }
236                            },
237                        }
238                        p { class: "text-xs text-[var(--color-paper-secondary)]",
239                            "上传与动态处理的宽/高上限。默认 8192,下限 512。"
240                        }
241                    }
242
243                    div { class: "flex flex-col gap-2 max-w-xl",
244                        FormLabel {
245                            label: "总像素上限",
246                            html_for: Some("img-px".to_string()),
247                        }
248                        input {
249                            id: "img-px",
250                            r#type: "number",
251                            min: "1000000",
252                            class: "{INPUT_CLASS}",
253                            value: "{img_draft().max_pixels}",
254                            oninput: move |e: Event<FormData>| {
255                                let v = e.value();
256                                if let Ok(n) = v.parse::<u64>() {
257                                    let mut d = img_draft();
258                                    d.max_pixels = n;
259                                    img_draft.set(d);
260                                    img_just_saved.set(false);
261                                }
262                            },
263                        }
264                        p { class: "text-xs text-[var(--color-paper-secondary)]",
265                            "决定单图解码内存(pixels×4+1MB)。默认 50M(约 7000×7000)。"
266                        }
267                    }
268
269                    div { class: "flex flex-col gap-2 max-w-xl",
270                        FormLabel {
271                            label: "尺寸缓存 TTL(秒)",
272                            html_for: Some("img-ttl".to_string()),
273                        }
274                        input {
275                            id: "img-ttl",
276                            r#type: "number",
277                            min: "1",
278                            class: "{INPUT_CLASS}",
279                            value: "{img_draft().dimensions_cache_ttl_secs}",
280                            oninput: move |e: Event<FormData>| {
281                                let v = e.value();
282                                if let Ok(n) = v.parse::<u64>() {
283                                    let mut d = img_draft();
284                                    d.dimensions_cache_ttl_secs = n;
285                                    img_draft.set(d);
286                                    img_just_saved.set(false);
287                                }
288                            },
289                        }
290                        p { class: "text-xs text-[var(--color-paper-secondary)]",
291                            "图片宽高缓存的 TTL,用于生成 aspect-ratio。默认 86400(24h)。"
292                        }
293                    }
294
295                    div { class: "flex items-center justify-between gap-4 pt-1",
296                        if img_just_saved() {
297                            span { class: "inline-flex items-center gap-1.5 text-xs text-[var(--color-paper-accent)]",
298                                svg {
299                                    class: "w-3.5 h-3.5",
300                                    view_box: "0 0 24 24",
301                                    fill: "none",
302                                    stroke: "currentColor",
303                                    stroke_width: "2.5",
304                                    path {
305                                        stroke_linecap: "round",
306                                        stroke_linejoin: "round",
307                                        d: "M5 13l4 4L19 7",
308                                    }
309                                }
310                                "已保存(重启后生效)"
311                            }
312                        } else if img_dirty() {
313                            span { class: "text-xs text-[var(--color-paper-secondary)]",
314                                "有未保存的更改"
315                            }
316                        } else {
317                            span { class: "text-xs text-transparent select-none", "·" }
318                        }
319                        LoadingButton {
320                            label: "保存设置".to_string(),
321                            loading: img_saving(),
322                            disabled: img_loading() || img_just_saved() || !img_dirty(),
323                            onclick: move |_| {
324                                let d = img_draft();
325                                img_saving.set(true);
326                                spawn(async move {
327                                    match update_image_limit_settings(
328                                            d.max_dimension,
329                                            d.max_pixels,
330                                            d.dimensions_cache_ttl_secs,
331                                        )
332                                        .await
333                                    {
334                                        Ok(s) => {
335                                            img_saved.set(s.clone());
336                                            img_draft.set(s);
337                                            img_just_saved.set(true);
338                                            toast.call(("保存成功".to_string(), false));
339                                        }
340                                        Err(e) => toast.call((format!("保存失败:{e}"), true)),
341                                    }
342                                    img_saving.set(false);
343                                });
344                            },
345                        }
346                    }
347                }
348            }
349        }
350    }
351    #[cfg(not(target_arch = "wasm32"))]
352    {
353        rsx! {
354            div { class: "p-8 text-[var(--color-paper-secondary)]", "图片配置(前端渲染)" }
355        }
356    }
357}