yggdrasil/pages/admin/settings/
ratelimit_section.rs1use dioxus::prelude::*;
7
8#[cfg(target_arch = "wasm32")]
9use crate::api::settings::{get_rate_limit_settings, update_rate_limit_settings};
10#[cfg(target_arch = "wasm32")]
11use crate::components::forms::{FormLabel, INPUT_CLASS};
12#[cfg(target_arch = "wasm32")]
13use crate::components::ui::{LoadingButton, ADMIN_CARD_CLASS};
14#[cfg(target_arch = "wasm32")]
15use crate::models::settings::RateLimitSettings;
16
17#[allow(non_snake_case)]
19#[component]
20pub fn RateLimitSection(toast: Callback<(String, bool)>) -> Element {
21 #[cfg(target_arch = "wasm32")]
22 {
23 let mut saved: Signal<RateLimitSettings> = use_signal(RateLimitSettings::default);
24 let mut draft: Signal<RateLimitSettings> = use_signal(RateLimitSettings::default);
25 let loading = use_signal(|| true);
26 let mut saving = use_signal(|| false);
27 let mut just_saved = use_signal(|| false);
28
29 use_effect(move || {
30 #[cfg(target_arch = "wasm32")]
31 {
32 let mut saved = saved;
33 let mut draft = draft;
34 let mut loading = loading;
35 spawn(async move {
36 match get_rate_limit_settings().await {
37 Ok(s) => {
38 saved.set(s.clone());
39 draft.set(s);
40 }
41 Err(e) => toast.call((format!("加载失败:{e}"), true)),
42 }
43 loading.set(false);
44 });
45 }
46 });
47
48 let dirty = use_memo(move || draft() != saved());
49
50 rsx! {
51 div { class: "space-y-6",
52 div { class: "{ADMIN_CARD_CLASS} p-6 md:p-8 flex flex-col gap-6",
53 div { class: "flex items-center gap-3",
54 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)]",
55 svg {
56 class: "w-5 h-5",
57 view_box: "0 0 24 24",
58 fill: "none",
59 stroke: "currentColor",
60 stroke_width: "1.8",
61 stroke_linecap: "round",
62 stroke_linejoin: "round",
63 path { d: "M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2z M12 6v6l4 2" }
64 }
65 }
66 div {
67 h2 { class: "text-xl font-bold text-[var(--color-paper-primary)]",
68 "接口限流"
69 }
70 p { class: "text-sm text-[var(--color-paper-secondary)] mt-0.5",
71 span { class: "text-amber-600 dark:text-amber-400 font-medium",
72 "⚠ 修改后需重启服务生效。"
73 }
74 }
75 }
76 }
77
78 LimiterField {
80 label: "严格限流(登录/注册)",
81 per_sec: draft().strict_per_sec,
82 burst: draft().strict_burst,
83 on_per_sec: move |v| {
84 let mut d = draft();
85 d.strict_per_sec = v;
86 draft.set(d);
87 just_saved.set(false);
88 },
89 on_burst: move |v| {
90 let mut d = draft();
91 d.strict_burst = v;
92 draft.set(d);
93 just_saved.set(false);
94 },
95 }
96 LimiterField {
97 label: "上传限流(图片上传)",
98 per_sec: draft().upload_per_sec,
99 burst: draft().upload_burst,
100 on_per_sec: move |v| {
101 let mut d = draft();
102 d.upload_per_sec = v;
103 draft.set(d);
104 just_saved.set(false);
105 },
106 on_burst: move |v| {
107 let mut d = draft();
108 d.upload_burst = v;
109 draft.set(d);
110 just_saved.set(false);
111 },
112 }
113 LimiterField {
114 label: "图片访问限流(GET /uploads/*)",
115 per_sec: draft().image_per_sec,
116 burst: draft().image_burst,
117 on_per_sec: move |v| {
118 let mut d = draft();
119 d.image_per_sec = v;
120 draft.set(d);
121 just_saved.set(false);
122 },
123 on_burst: move |v| {
124 let mut d = draft();
125 d.image_burst = v;
126 draft.set(d);
127 just_saved.set(false);
128 },
129 }
130 LimiterField {
131 label: "评论限流(创建评论)",
132 per_sec: draft().comment_per_sec,
133 burst: draft().comment_burst,
134 on_per_sec: move |v| {
135 let mut d = draft();
136 d.comment_per_sec = v;
137 draft.set(d);
138 just_saved.set(false);
139 },
140 on_burst: move |v| {
141 let mut d = draft();
142 d.comment_burst = v;
143 draft.set(d);
144 just_saved.set(false);
145 },
146 }
147 LimiterField {
148 label: "评论图片上传限流(含匿名)",
149 per_sec: draft().comment_upload_per_sec,
150 burst: draft().comment_upload_burst,
151 on_per_sec: move |v| {
152 let mut d = draft();
153 d.comment_upload_per_sec = v;
154 draft.set(d);
155 just_saved.set(false);
156 },
157 on_burst: move |v| {
158 let mut d = draft();
159 d.comment_upload_burst = v;
160 draft.set(d);
161 just_saved.set(false);
162 },
163 }
164 LimiterField {
165 label: "代码执行限流",
166 per_sec: draft().code_exec_per_sec,
167 burst: draft().code_exec_burst,
168 on_per_sec: move |v| {
169 let mut d = draft();
170 d.code_exec_per_sec = v;
171 draft.set(d);
172 just_saved.set(false);
173 },
174 on_burst: move |v| {
175 let mut d = draft();
176 d.code_exec_burst = v;
177 draft.set(d);
178 just_saved.set(false);
179 },
180 }
181 LimiterField {
182 label: "unknown 桶(无法识别 IP)",
183 per_sec: draft().unknown_per_sec,
184 burst: draft().unknown_burst,
185 on_per_sec: move |v| {
186 let mut d = draft();
187 d.unknown_per_sec = v;
188 draft.set(d);
189 just_saved.set(false);
190 },
191 on_burst: move |v| {
192 let mut d = draft();
193 d.unknown_burst = v;
194 draft.set(d);
195 just_saved.set(false);
196 },
197 }
198
199 div { class: "flex flex-col gap-2 max-w-xl",
201 FormLabel {
202 label: "评论图片上传日限额(次/天,按 IP)",
203 html_for: Some("rl-comment-upload-daily".to_string()),
204 }
205 input {
206 id: "rl-comment-upload-daily",
207 r#type: "number",
208 min: "1",
209 class: "{INPUT_CLASS}",
210 value: "{draft().comment_upload_daily}",
211 oninput: move |e: Event<FormData>| {
212 let v = e.value();
213 if let Ok(n) = v.parse::<u32>() {
214 let mut d = draft();
215 d.comment_upload_daily = n;
216 draft.set(d);
217 just_saved.set(false);
218 }
219 },
220 }
221 p { class: "text-xs text-[var(--color-paper-secondary)]",
222 "评论区允许匿名传图,日限额是防磁盘耗尽的关键防线。"
223 }
224 }
225 div { class: "flex flex-col gap-2 max-w-xl",
226 FormLabel {
227 label: "代码执行日限额(次/天)",
228 html_for: Some("rl-code-daily".to_string()),
229 }
230 input {
231 id: "rl-code-daily",
232 r#type: "number",
233 min: "1",
234 class: "{INPUT_CLASS}",
235 value: "{draft().code_exec_daily}",
236 oninput: move |e: Event<FormData>| {
237 let v = e.value();
238 if let Ok(n) = v.parse::<u32>() {
239 let mut d = draft();
240 d.code_exec_daily = n;
241 draft.set(d);
242 just_saved.set(false);
243 }
244 },
245 }
246 }
247 div { class: "flex flex-col gap-2 max-w-xl",
248 FormLabel {
249 label: "限流桶 GC 间隔(秒)",
250 html_for: Some("rl-gc".to_string()),
251 }
252 input {
253 id: "rl-gc",
254 r#type: "number",
255 min: "1",
256 class: "{INPUT_CLASS}",
257 value: "{draft().gc_interval_secs}",
258 oninput: move |e: Event<FormData>| {
259 let v = e.value();
260 if let Ok(n) = v.parse::<u32>() {
261 let mut d = draft();
262 d.gc_interval_secs = n;
263 draft.set(d);
264 just_saved.set(false);
265 }
266 },
267 }
268 p { class: "text-xs text-[var(--color-paper-secondary)]",
269 "周期性回收已冷却的 IP 键,防止键空间无限膨胀。"
270 }
271 }
272
273 div { class: "flex items-center justify-between gap-4 pt-1",
274 if just_saved() {
275 span { class: "inline-flex items-center gap-1.5 text-xs text-[var(--color-paper-accent)]",
276 svg {
277 class: "w-3.5 h-3.5",
278 view_box: "0 0 24 24",
279 fill: "none",
280 stroke: "currentColor",
281 stroke_width: "2.5",
282 path {
283 stroke_linecap: "round",
284 stroke_linejoin: "round",
285 d: "M5 13l4 4L19 7",
286 }
287 }
288 "已保存(重启后生效)"
289 }
290 } else if dirty() {
291 span { class: "text-xs text-[var(--color-paper-secondary)]",
292 "有未保存的更改"
293 }
294 } else {
295 span { class: "text-xs text-transparent select-none", "·" }
296 }
297 LoadingButton {
298 label: "保存设置".to_string(),
299 loading: saving(),
300 disabled: loading() || just_saved() || !dirty(),
301 onclick: move |_| {
302 let d = draft();
303 saving.set(true);
304 spawn(async move {
305 match update_rate_limit_settings(
306 d.strict_per_sec,
307 d.strict_burst,
308 d.upload_per_sec,
309 d.upload_burst,
310 d.image_per_sec,
311 d.image_burst,
312 d.comment_per_sec,
313 d.comment_burst,
314 d.comment_upload_per_sec,
315 d.comment_upload_burst,
316 d.comment_upload_daily,
317 d.code_exec_per_sec,
318 d.code_exec_burst,
319 d.code_exec_daily,
320 d.unknown_per_sec,
321 d.unknown_burst,
322 d.gc_interval_secs,
323 )
324 .await
325 {
326 Ok(s) => {
327 saved.set(s.clone());
328 draft.set(s);
329 just_saved.set(true);
330 toast.call(("保存成功".to_string(), false));
331 }
332 Err(e) => toast.call((format!("保存失败:{e}"), true)),
333 }
334 saving.set(false);
335 });
336 },
337 }
338 }
339 }
340 }
341 }
342 }
343 #[cfg(not(target_arch = "wasm32"))]
344 {
345 rsx! {
346 div { class: "p-8 text-[var(--color-paper-secondary)]", "限流配置(前端渲染)" }
347 }
348 }
349}
350#[cfg(target_arch = "wasm32")]
351#[allow(non_snake_case)]
353#[component]
354fn LimiterField(
355 label: String,
356 per_sec: u32,
357 burst: u32,
358 on_per_sec: Callback<u32>,
359 on_burst: Callback<u32>,
360) -> Element {
361 rsx! {
362 div { class: "flex flex-col gap-2 max-w-xl",
363 FormLabel { label: "{label}", html_for: None }
364 div { class: "flex gap-3",
365 div { class: "flex-1",
366 input {
367 r#type: "number",
368 min: "1",
369 class: "{INPUT_CLASS}",
370 value: "{per_sec}",
371 oninput: move |e: Event<FormData>| {
372 let v = e.value();
373 if let Ok(n) = v.parse::<u32>() {
374 on_per_sec.call(n);
375 }
376 },
377 }
378 p { class: "text-xs text-[var(--color-paper-tertiary)] mt-1",
379 "每秒请求数"
380 }
381 }
382 div { class: "flex-1",
383 input {
384 r#type: "number",
385 min: "1",
386 class: "{INPUT_CLASS}",
387 value: "{burst}",
388 oninput: move |e: Event<FormData>| {
389 let v = e.value();
390 if let Ok(n) = v.parse::<u32>() {
391 on_burst.call(n);
392 }
393 },
394 }
395 p { class: "text-xs text-[var(--color-paper-tertiary)] mt-1",
396 "突发上限"
397 }
398 }
399 }
400 }
401 }
402}