yggdrasil/pages/admin/settings/
image_section.rs1use 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#[allow(non_snake_case)]
20#[component]
21pub fn ImageSection(toast: Callback<(String, bool)>) -> Element {
22 #[cfg(target_arch = "wasm32")]
23 {
24 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 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 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 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}