refactor(trash): 抽取 AutoPurgeSettings 子组件,降低 TrashPage 复杂度

TrashPage 主体从约 484 行降至约 266 行。自动清理配置(可折叠设置面板:
开关 + 保留天数步进 + 保存 + 已保存反馈)整体迁入 AutoPurgeSettings 子组件,
含 6 个私有 signal(draft_days/enabled、panel_open、saving、just_saved)+ dirty
memo + chevron_rotate 派生。

接口:settings(已保存配置)作为双向绑定 signal prop——子组件加载/保存时写入,
父组件读取 retention_days 供 TrashRow 的「剩余天数」展示。比 CoverUploader 的
两 signal 接口更简。

附带简化:原 settings_loaded 守卫删除——子组件挂载即触发一次自身 effect 加载
配置,不再嵌入列表翻页 effect,逻辑更清晰。

依据 dioxus-render-purity skill 的 god component 治理建议。验证:cargo build +
dx check + cargo test (406) 全绿。
This commit is contained in:
xfy 2026-06-26 10:31:23 +08:00
parent 2b5cd5f64f
commit a534ee436f

View File

@ -53,18 +53,11 @@ pub fn TrashPage(page: i32) -> Element {
let mut loading: Signal<bool> = use_signal(|| false);
#[allow(unused_mut)]
let mut error: Signal<Option<String>> = use_signal(|| None);
// 自动清理配置(含本地草稿态用于表单输入)。
// 自动清理配置:由子组件 AutoPurgeSettings 写入(加载/保存),本组件读取
// retention_days 供 TrashRow 的「剩余天数」展示。
let mut settings: Signal<TrashSettings> = use_signal(TrashSettings::default);
let mut settings_draft_days: Signal<String> = use_signal(|| "30".to_string());
let mut settings_draft_enabled: Signal<bool> = use_signal(|| false);
let mut settings_panel_open: Signal<bool> = use_signal(|| false);
let mut saving_settings: Signal<bool> = use_signal(|| false);
// 保存成功后的短暂反馈标记(用户再次编辑时清除)。
let mut just_saved: Signal<bool> = use_signal(|| false);
// 配置只加载一次的标记,避免每次翻页 effect 重复拉取。
let mut settings_loaded: Signal<bool> = use_signal(|| false);
// 加载回收站列表;配置仅首次加载
// 加载回收站列表(自动清理配置由子组件 AutoPurgeSettings 自行加载)。
use_effect(move || {
let _ = current_page;
loading.set(true);
@ -82,16 +75,6 @@ pub fn TrashPage(page: i32) -> Element {
}
loading.set(false);
});
if !settings_loaded() {
settings_loaded.set(true);
spawn(async move {
if let Ok(s) = get_trash_settings().await {
settings_draft_days.set(s.retention_days.to_string());
settings_draft_enabled.set(s.auto_purge_enabled);
settings.set(s);
}
});
}
}
#[cfg(not(target_arch = "wasm32"))]
{
@ -108,24 +91,6 @@ pub fn TrashPage(page: i32) -> Element {
});
};
// 草稿相对已保存配置是否存在差异:控制保存按钮可用性与“未保存”提示。
// 派生值用 use_memo依赖信号不变时不重算避免每次渲染重复 parse 字符串)。
let dirty = use_memo(move || {
settings_draft_enabled() != settings().auto_purge_enabled
|| settings_draft_days()
.trim()
.parse::<i32>()
.ok()
.map(|d| d != settings().retention_days)
.unwrap_or(true)
});
// 折叠箭头旋转类(展开时翻转 180°
let chevron_rotate = if settings_panel_open() {
"rotate-180"
} else {
""
};
rsx! {
div { class: "space-y-6",
// 页面标题
@ -134,189 +99,9 @@ pub fn TrashPage(page: i32) -> Element {
span { class: "text-sm text-paper-secondary", "共 {total()} 篇" }
}
// 自动清理配置卡片:可折叠的设置面板,顶部始终显示当前状态摘要
div { class: "rounded-xl border border-paper-border overflow-hidden bg-paper-entry",
// 顶部可点击摘要条:状态指示灯 + 标题 + 展开箭头
button {
class: "w-full flex items-center gap-3 px-5 py-4 text-left cursor-pointer hover:bg-paper-theme focus:outline-none focus-visible:ring-2 focus-visible:ring-paper-accent/40",
onclick: move |_| {
settings_panel_open.set(!settings_panel_open());
just_saved.set(false);
},
// 状态指示灯
{
let dot_class = if settings().auto_purge_enabled {
"w-2 h-2 rounded-full bg-paper-accent shadow-[0_0_0_3px_rgba(92,122,94,0.15)]"
} else {
"w-2 h-2 rounded-full bg-paper-tertiary"
};
rsx! {
div { class: "w-2 flex-shrink-0 flex items-center justify-center",
div { class: "{dot_class}" }
}
}
}
// 标题 + 当前状态描述
div { class: "flex-1 min-w-0",
div { class: "text-sm font-medium text-paper-primary", "自动清理" }
div { class: "text-xs text-paper-secondary mt-0.5 truncate",
if settings().auto_purge_enabled {
"已开启 · 超过 {settings().retention_days} 天的文章将被自动删除"
} else {
"已关闭"
}
}
}
// 展开箭头(旋转动画)
svg {
class: "w-4 h-4 text-paper-secondary transition-transform duration-200 flex-shrink-0 {chevron_rotate}",
view_box: "0 0 24 24",
fill: "none",
stroke: "currentColor",
stroke_width: "2",
path {
stroke_linecap: "round",
stroke_linejoin: "round",
d: "M19 9l-7 7-7-7",
}
}
}
// 自动清理配置卡片(抽取为子组件 AutoPurgeSettings见文件末尾
AutoPurgeSettings { settings }
// 设置面板(可折叠)
if settings_panel_open() {
div { class: "border-t border-paper-border p-5 space-y-6",
// 开关行:启用自动清理
div { class: "flex items-center justify-between gap-4",
div { class: "min-w-0",
div { class: "text-sm font-medium text-paper-primary",
"启用自动清理"
}
div { class: "text-xs text-paper-secondary mt-1",
"后台任务定期彻底删除超过保留期的文章"
}
}
// 自定义开关toggle switch—— 取代原生 checkbox
button {
role: "switch",
aria_checked: "{settings_draft_enabled()}",
class: if settings_draft_enabled() { "relative w-11 h-6 flex-shrink-0 rounded-full bg-paper-accent cursor-pointer transition-colors duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-paper-accent/40" } else { "relative w-11 h-6 flex-shrink-0 rounded-full bg-paper-tertiary cursor-pointer transition-colors duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-paper-accent/40" },
onclick: move |_| {
settings_draft_enabled.set(!settings_draft_enabled());
just_saved.set(false);
},
// 滑块圆点
span { class: if settings_draft_enabled() { "absolute top-0.5 left-0.5 w-5 h-5 rounded-full bg-white shadow-sm dark:shadow-black/30 transition-transform duration-200 translate-x-5" } else { "absolute top-0.5 left-0.5 w-5 h-5 rounded-full bg-white shadow-sm dark:shadow-black/30 transition-transform duration-200" } }
}
}
// 保留天数行
div { class: "space-y-3",
div { class: "min-w-0",
div { class: "text-sm font-medium text-paper-primary",
"保留天数"
}
div { class: "text-xs text-paper-secondary mt-1",
"文章删除后保留的时长到期后自动彻底清除1365"
}
}
// 数字输入 + 步进按钮 + 单位后缀
div { class: "flex items-center gap-3",
div { class: "flex items-center rounded-lg border border-paper-border bg-paper-entry overflow-hidden",
// 减号
button {
class: "w-9 h-9 flex items-center justify-center text-sm text-paper-secondary hover:text-paper-primary hover:bg-paper-theme cursor-pointer transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-paper-accent/40",
r#type: "button",
aria_label: "减少保留天数",
onclick: move |_| {
let cur: i32 = settings_draft_days().trim().parse().unwrap_or(30);
let next = cur.saturating_sub(1).max(1);
settings_draft_days.set(next.to_string());
just_saved.set(false);
},
""
}
// 数字输入(无边框,衔接步进按钮)
input {
r#type: "number",
min: "1",
max: "365",
class: "w-14 h-9 px-1 text-center text-sm tabular-nums text-paper-primary bg-transparent border-0 focus:outline-none [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none",
value: "{settings_draft_days()}",
oninput: move |e| {
settings_draft_days.set(e.value());
just_saved.set(false);
},
}
// 加号
button {
class: "w-9 h-9 flex items-center justify-center text-sm text-paper-secondary hover:text-paper-primary hover:bg-paper-theme cursor-pointer transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-paper-accent/40",
r#type: "button",
aria_label: "增加保留天数",
onclick: move |_| {
let cur: i32 = settings_draft_days().trim().parse().unwrap_or(30);
let next = cur.saturating_add(1).min(365);
settings_draft_days.set(next.to_string());
just_saved.set(false);
},
"+"
}
}
span { class: "text-xs text-paper-secondary", "" }
}
}
// 底部操作行:未保存提示 + 保存按钮
div { class: "flex items-center justify-between gap-4 pt-1",
// 草稿状态提示
if just_saved() {
span { class: "inline-flex items-center gap-1.5 text-xs text-paper-accent",
svg {
class: "w-3.5 h-3.5",
view_box: "0 0 24 24",
fill: "none",
stroke: "currentColor",
stroke_width: "2.5",
path {
stroke_linecap: "round",
stroke_linejoin: "round",
d: "M5 13l4 4L19 7",
}
}
"已保存"
}
} else if dirty() {
span { class: "text-xs text-paper-secondary", "有未保存的更改" }
} else {
span { class: "text-xs text-transparent select-none",
"·"
}
}
// 保存按钮:启用主题色,禁用/保存中态灰化
button {
class: if saving_settings() { "inline-flex items-center gap-1.5 px-4 py-1.5 text-sm font-medium cursor-not-allowed text-paper-secondary bg-paper-tertiary rounded-full" } else if just_saved() { "inline-flex items-center gap-1.5 px-4 py-1.5 text-sm font-medium cursor-not-allowed text-paper-secondary bg-paper-tertiary rounded-full" } else { "inline-flex items-center gap-1.5 px-4 py-1.5 text-sm font-medium text-paper-theme bg-paper-accent rounded-full hover:brightness-110 active:scale-[0.98] transition-all cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-paper-accent/40" },
disabled: saving_settings() || just_saved() || !dirty(),
onclick: move |_| {
let days: i32 = settings_draft_days().parse().unwrap_or(30);
let enabled = settings_draft_enabled();
saving_settings.set(true);
spawn(async move {
if let Ok(s) = update_trash_settings(enabled, days).await {
settings.set(s);
just_saved.set(true);
}
saving_settings.set(false);
});
},
if saving_settings() {
"保存中…"
} else {
"保存设置"
}
}
}
}
}
}
// 批量操作栏(选中时显示)
if !selected_ids().is_empty() {
@ -530,6 +315,241 @@ pub fn TrashPage(page: i32) -> Element {
}
}
/// 自动清理配置子组件:可折叠的设置面板。
///
/// 封装自动清理的全部状态:表单草稿(`settings_draft_*`)、面板折叠态、保存态、
/// 已保存反馈、以及派生的 `dirty` / `chevron_rotate`。配置加载与保存均在组件
/// 内部完成。`settings`(已保存的服务端配置)由父组件传入双向绑定 signal
/// 本组件加载/保存时写入,父组件读取 `retention_days` 供 TrashRow 的「剩余天数」。
///
/// 从 `TrashPage` 抽取以降低 god component 复杂度(见 dioxus-render-purity skill
#[component]
#[cfg_attr(not(target_arch = "wasm32"), allow(unused_mut, unused_variables))]
fn AutoPurgeSettings(settings: Signal<TrashSettings>) -> Element {
let mut settings_draft_days: Signal<String> = use_signal(|| "30".to_string());
let mut settings_draft_enabled: Signal<bool> = use_signal(|| false);
let mut settings_panel_open: Signal<bool> = use_signal(|| false);
let mut saving_settings: Signal<bool> = use_signal(|| false);
// 保存成功后的短暂反馈标记(用户再次编辑时清除)。
let mut just_saved: Signal<bool> = use_signal(|| false);
// 首次渲染加载服务端配置:本组件挂载即触发一次,无需 settings_loaded 守卫
//(父组件每次翻页重渲染的是列表 effect本组件 effect 只在自身首次挂载跑)。
use_effect(move || {
#[cfg(target_arch = "wasm32")]
spawn(async move {
if let Ok(s) = get_trash_settings().await {
settings_draft_days.set(s.retention_days.to_string());
settings_draft_enabled.set(s.auto_purge_enabled);
settings.set(s);
}
});
});
// 草稿相对已保存配置是否存在差异:控制保存按钮可用性与“未保存”提示。
// 派生值用 use_memo依赖信号不变时不重算避免每次渲染重复 parse 字符串)。
let dirty = use_memo(move || {
settings_draft_enabled() != settings().auto_purge_enabled
|| settings_draft_days()
.trim()
.parse::<i32>()
.ok()
.map(|d| d != settings().retention_days)
.unwrap_or(true)
});
// 折叠箭头旋转类(展开时翻转 180°
let chevron_rotate = if settings_panel_open() {
"rotate-180"
} else {
""
};
rsx! {
div { class: "rounded-xl border border-paper-border overflow-hidden bg-paper-entry",
// 顶部可点击摘要条:状态指示灯 + 标题 + 展开箭头
button {
class: "w-full flex items-center gap-3 px-5 py-4 text-left cursor-pointer hover:bg-paper-theme focus:outline-none focus-visible:ring-2 focus-visible:ring-paper-accent/40",
onclick: move |_| {
settings_panel_open.set(!settings_panel_open());
just_saved.set(false);
},
// 状态指示灯
{
let dot_class = if settings().auto_purge_enabled {
"w-2 h-2 rounded-full bg-paper-accent shadow-[0_0_0_3px_rgba(92,122,94,0.15)]"
} else {
"w-2 h-2 rounded-full bg-paper-tertiary"
};
rsx! {
div { class: "w-2 flex-shrink-0 flex items-center justify-center",
div { class: "{dot_class}" }
}
}
}
// 标题 + 当前状态描述
div { class: "flex-1 min-w-0",
div { class: "text-sm font-medium text-paper-primary", "自动清理" }
div { class: "text-xs text-paper-secondary mt-0.5 truncate",
if settings().auto_purge_enabled {
"已开启 · 超过 {settings().retention_days} 天的文章将被自动删除"
} else {
"已关闭"
}
}
}
// 展开箭头(旋转动画)
svg {
class: "w-4 h-4 text-paper-secondary transition-transform duration-200 flex-shrink-0 {chevron_rotate}",
view_box: "0 0 24 24",
fill: "none",
stroke: "currentColor",
stroke_width: "2",
path {
stroke_linecap: "round",
stroke_linejoin: "round",
d: "M19 9l-7 7-7-7",
}
}
}
// 设置面板(可折叠)
if settings_panel_open() {
div { class: "border-t border-paper-border p-5 space-y-6",
// 开关行:启用自动清理
div { class: "flex items-center justify-between gap-4",
div { class: "min-w-0",
div { class: "text-sm font-medium text-paper-primary",
"启用自动清理"
}
div { class: "text-xs text-paper-secondary mt-1",
"后台任务定期彻底删除超过保留期的文章"
}
}
// 自定义开关toggle switch—— 取代原生 checkbox
button {
role: "switch",
aria_checked: "{settings_draft_enabled()}",
class: if settings_draft_enabled() { "relative w-11 h-6 flex-shrink-0 rounded-full bg-paper-accent cursor-pointer transition-colors duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-paper-accent/40" } else { "relative w-11 h-6 flex-shrink-0 rounded-full bg-paper-tertiary cursor-pointer transition-colors duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-paper-accent/40" },
onclick: move |_| {
settings_draft_enabled.set(!settings_draft_enabled());
just_saved.set(false);
},
// 滑块圆点
span { class: if settings_draft_enabled() { "absolute top-0.5 left-0.5 w-5 h-5 rounded-full bg-white shadow-sm dark:shadow-black/30 transition-transform duration-200 translate-x-5" } else { "absolute top-0.5 left-0.5 w-5 h-5 rounded-full bg-white shadow-sm dark:shadow-black/30 transition-transform duration-200" } }
}
}
// 保留天数行
div { class: "space-y-3",
div { class: "min-w-0",
div { class: "text-sm font-medium text-paper-primary",
"保留天数"
}
div { class: "text-xs text-paper-secondary mt-1",
"文章删除后保留的时长到期后自动彻底清除1365"
}
}
// 数字输入 + 步进按钮 + 单位后缀
div { class: "flex items-center gap-3",
div { class: "flex items-center rounded-lg border border-paper-border bg-paper-entry overflow-hidden",
// 减号
button {
class: "w-9 h-9 flex items-center justify-center text-sm text-paper-secondary hover:text-paper-primary hover:bg-paper-theme cursor-pointer transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-paper-accent/40",
r#type: "button",
aria_label: "减少保留天数",
onclick: move |_| {
let cur: i32 = settings_draft_days().trim().parse().unwrap_or(30);
let next = cur.saturating_sub(1).max(1);
settings_draft_days.set(next.to_string());
just_saved.set(false);
},
""
}
// 数字输入(无边框,衔接步进按钮)
input {
r#type: "number",
min: "1",
max: "365",
class: "w-14 h-9 px-1 text-center text-sm tabular-nums text-paper-primary bg-transparent border-0 focus:outline-none [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none",
value: "{settings_draft_days()}",
oninput: move |e| {
settings_draft_days.set(e.value());
just_saved.set(false);
},
}
// 加号
button {
class: "w-9 h-9 flex items-center justify-center text-sm text-paper-secondary hover:text-paper-primary hover:bg-paper-theme cursor-pointer transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-paper-accent/40",
r#type: "button",
aria_label: "增加保留天数",
onclick: move |_| {
let cur: i32 = settings_draft_days().trim().parse().unwrap_or(30);
let next = cur.saturating_add(1).min(365);
settings_draft_days.set(next.to_string());
just_saved.set(false);
},
"+"
}
}
span { class: "text-xs text-paper-secondary", "" }
}
}
// 底部操作行:未保存提示 + 保存按钮
div { class: "flex items-center justify-between gap-4 pt-1",
// 草稿状态提示
if just_saved() {
span { class: "inline-flex items-center gap-1.5 text-xs text-paper-accent",
svg {
class: "w-3.5 h-3.5",
view_box: "0 0 24 24",
fill: "none",
stroke: "currentColor",
stroke_width: "2.5",
path {
stroke_linecap: "round",
stroke_linejoin: "round",
d: "M5 13l4 4L19 7",
}
}
"已保存"
}
} else if dirty() {
span { class: "text-xs text-paper-secondary", "有未保存的更改" }
} else {
span { class: "text-xs text-transparent select-none",
"·"
}
}
// 保存按钮:启用主题色,禁用/保存中态灰化
button {
class: if saving_settings() { "inline-flex items-center gap-1.5 px-4 py-1.5 text-sm font-medium cursor-not-allowed text-paper-secondary bg-paper-tertiary rounded-full" } else if just_saved() { "inline-flex items-center gap-1.5 px-4 py-1.5 text-sm font-medium cursor-not-allowed text-paper-secondary bg-paper-tertiary rounded-full" } else { "inline-flex items-center gap-1.5 px-4 py-1.5 text-sm font-medium text-paper-theme bg-paper-accent rounded-full hover:brightness-110 active:scale-[0.98] transition-all cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-paper-accent/40" },
disabled: saving_settings() || just_saved() || !dirty(),
onclick: move |_| {
let days: i32 = settings_draft_days().parse().unwrap_or(30);
let enabled = settings_draft_enabled();
saving_settings.set(true);
spawn(async move {
if let Ok(s) = update_trash_settings(enabled, days).await {
settings.set(s);
just_saved.set(true);
}
saving_settings.set(false);
});
},
if saving_settings() {
"保存中…"
} else {
"保存设置"
}
}
}
}
}
}
}
}
/// 计算剩余天数(保留期 - 已删除天数)。
///
/// 返回 (剩余天数, 是否已过期)。基于客户端时钟计算,轻微漂移可接受。