Skip to main content

yggdrasil/pages/admin/settings/
purge_section.rs

1//! 孤儿素材自动清理分区(启用开关 + 保留天数)。
2//!
3//! 后台任务每天读取 DB 值执行清理:无文章引用且无存活评论引用的素材,
4//! 超过保留天数后物理删除(文件 + DB 行 + 派生缓存)。
5
6use dioxus::prelude::*;
7
8#[cfg(target_arch = "wasm32")]
9use crate::api::settings::{get_asset_purge_settings, update_asset_purge_settings};
10#[cfg(target_arch = "wasm32")]
11use crate::components::forms::{FormLabel, 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::AssetPurgeSettings;
16
17/// 孤儿素材自动清理分区组件。
18#[allow(non_snake_case)]
19#[component]
20pub fn AssetPurgeSection(toast: Callback<(String, bool)>) -> Element {
21    #[cfg(target_arch = "wasm32")]
22    {
23        let mut saved: Signal<AssetPurgeSettings> = use_signal(AssetPurgeSettings::default);
24        let mut enabled_draft: Signal<bool> = use_signal(|| true);
25        let mut days_draft: Signal<i32> = use_signal(|| 7);
26        let loading = use_signal(|| true);
27        let mut saving = use_signal(|| false);
28        let mut just_saved = use_signal(|| false);
29
30        use_effect(move || {
31            #[cfg(target_arch = "wasm32")]
32            {
33                let mut saved = saved;
34                let mut enabled_draft = enabled_draft;
35                let mut days_draft = days_draft;
36                let mut loading = loading;
37                spawn(async move {
38                    match get_asset_purge_settings().await {
39                        Ok(s) => {
40                            saved.set(s.clone());
41                            enabled_draft.set(s.auto_purge_enabled);
42                            days_draft.set(s.retention_days);
43                        }
44                        Err(e) => toast.call((format!("加载失败:{e}"), true)),
45                    }
46                    loading.set(false);
47                });
48            }
49        });
50
51        let dirty = use_memo(move || {
52            enabled_draft() != saved().auto_purge_enabled || days_draft() != saved().retention_days
53        });
54
55        rsx! {
56            div { class: "space-y-6",
57                div { class: "{ADMIN_CARD_CLASS} p-6 md:p-8 flex flex-col gap-6",
58                    div { class: "flex items-center gap-3",
59                        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)]",
60                            svg {
61                                class: "w-5 h-5",
62                                view_box: "0 0 24 24",
63                                fill: "none",
64                                stroke: "currentColor",
65                                stroke_width: "1.8",
66                                stroke_linecap: "round",
67                                stroke_linejoin: "round",
68                                path { d: "M21 8v13H3V8 M1 3h22v5H1z M10 12h4" }
69                            }
70                        }
71                        div {
72                            h2 { class: "text-xl font-bold text-[var(--color-paper-primary)]",
73                                "孤儿素材自动清理"
74                            }
75                            p { class: "text-sm text-[var(--color-paper-secondary)] mt-0.5",
76                                "无文章/评论引用的图片超过保留天数后由后台任务物理删除(每天执行)。"
77                            }
78                        }
79                    }
80
81                    label { class: "flex items-center gap-3 cursor-pointer max-w-xl",
82                        Checkbox {
83                            checked: enabled_draft(),
84                            onchange: move |checked: bool| {
85                                enabled_draft.set(checked);
86                                just_saved.set(false);
87                            },
88                        }
89                        div { class: "flex flex-col",
90                            span { class: "text-sm font-medium text-[var(--color-paper-primary)]",
91                                "启用自动清理"
92                            }
93                            span { class: "text-xs text-[var(--color-paper-secondary)]",
94                                "关闭后未引用图片永久保留,需在素材管理页手动清理。评论区允许匿名传图,建议保持开启。"
95                            }
96                        }
97                    }
98
99                    div { class: "flex flex-col gap-2 max-w-xl",
100                        FormLabel {
101                            label: "保留天数",
102                            html_for: Some("asset-purge-days".to_string()),
103                        }
104                        input {
105                            id: "asset-purge-days",
106                            r#type: "number",
107                            min: "1",
108                            max: "365",
109                            class: "{INPUT_CLASS}",
110                            value: "{days_draft()}",
111                            oninput: move |e: Event<FormData>| {
112                                let v = e.value();
113                                if let Ok(n) = v.parse::<i32>() {
114                                    days_draft.set(n);
115                                }
116                                just_saved.set(false);
117                            },
118                        }
119                        p { class: "text-xs text-[var(--color-paper-secondary)]",
120                            "超出保留天数的未引用图片被物理删除(1–365 天)。保护窗防止误删尚未保存的草稿引用。"
121                        }
122                    }
123
124                    div { class: "flex items-center justify-between gap-4 pt-1",
125                        if just_saved() {
126                            span { class: "inline-flex items-center gap-1.5 text-xs text-[var(--color-paper-accent)]",
127                                svg {
128                                    class: "w-3.5 h-3.5",
129                                    view_box: "0 0 24 24",
130                                    fill: "none",
131                                    stroke: "currentColor",
132                                    stroke_width: "2.5",
133                                    path {
134                                        stroke_linecap: "round",
135                                        stroke_linejoin: "round",
136                                        d: "M5 13l4 4L19 7",
137                                    }
138                                }
139                                "已保存"
140                            }
141                        } else if dirty() {
142                            span { class: "text-xs text-[var(--color-paper-secondary)]",
143                                "有未保存的更改"
144                            }
145                        } else {
146                            span { class: "text-xs text-transparent select-none", "·" }
147                        }
148                        LoadingButton {
149                            label: "保存设置".to_string(),
150                            loading: saving(),
151                            disabled: loading() || just_saved() || !dirty(),
152                            onclick: move |_| {
153                                let e = enabled_draft();
154                                let d = days_draft();
155                                saving.set(true);
156                                spawn(async move {
157                                    match update_asset_purge_settings(e, d).await {
158                                        Ok(s) => {
159                                            saved.set(s.clone());
160                                            enabled_draft.set(s.auto_purge_enabled);
161                                            days_draft.set(s.retention_days);
162                                            just_saved.set(true);
163                                            toast.call(("保存成功".to_string(), false));
164                                        }
165                                        Err(err) => toast.call((format!("保存失败:{err}"), true)),
166                                    }
167                                    saving.set(false);
168                                });
169                            },
170                        }
171                    }
172                }
173            }
174        }
175    }
176    #[cfg(not(target_arch = "wasm32"))]
177    {
178        rsx! {
179            div { class: "p-8 text-[var(--color-paper-secondary)]", "孤儿素材清理配置(前端渲染)" }
180        }
181    }
182}