diff --git a/src/pages/admin/posts.rs b/src/pages/admin/posts.rs index ccd2837..c8c2754 100644 --- a/src/pages/admin/posts.rs +++ b/src/pages/admin/posts.rs @@ -57,8 +57,10 @@ pub fn PostsPage(page: i32) -> Element { let loading = paginated.loading; let _error = paginated.error; - // 删除中 ID、重建缓存状态与结果仍由本组件持有(业务逻辑不归 hook 管)。 - let mut deleting = use_signal(|| None::); + // 删除中 / 重建中文章 ID 集合:均由本组件持有(业务逻辑不归 hook 管)。 + // 改为非乐观删除后行会保留至请求完成,可并发点多个删除,故用 HashSet + // 与 rebuilding 同形,按行通过 contains 判断 loading 态。 + let mut deleting = use_signal(|| std::collections::HashSet::::new()); // 重建中文章 ID 集合:支持多篇文章并发重建(行不会随点击消失,单值会被后点 // 的覆盖先点的,故用 HashSet),按行通过 contains 判断 loading 态。 let mut rebuilding = use_signal(|| std::collections::HashSet::::new()); @@ -130,16 +132,18 @@ pub fn PostsPage(page: i32) -> Element { PostRow { key: "{post.id}", post: post.clone(), - deleting: deleting() == Some(post.id), + deleting: deleting().contains(&post.id), rebuilding: rebuilding().contains(&post.id), - // 删除文章:先乐观更新本地列表,再调用 server function,失败时弹出浏览器提示。 + // 删除文章:非乐观——先标记 deleting 让按钮显示 loading, + // 服务端成功后才移除行并减总数,失败则保留行并弹出浏览器提示。 on_delete: move |id| { - deleting.set(Some(id)); - let id_for_api = id; - posts.with_mut(|list| list.retain(|p| p.id != id)); - total.with_mut(|t| *t = t.saturating_sub(1)); + deleting.write().insert(id); spawn(async move { - match delete_post(id_for_api).await { + match delete_post(id).await { + Ok(CreatePostResponse { success: true, .. }) => { + posts.with_mut(|list| list.retain(|p| p.id != id)); + total.with_mut(|t| *t = t.saturating_sub(1)); + } Ok(CreatePostResponse { success: false, message: _message, .. }) => { #[cfg(target_arch = "wasm32")] web_sys::window().map(|w| w.alert_with_message(&_message).ok()); @@ -148,9 +152,8 @@ pub fn PostsPage(page: i32) -> Element { #[cfg(target_arch = "wasm32")] web_sys::window().map(|w| w.alert_with_message("删除失败").ok()); } - _ => {} } - deleting.set(None); + deleting.write().remove(&id); }); }, // 重建单篇文章内容:调用 server function 重新渲染 content_html。 @@ -334,14 +337,23 @@ fn PostRow( } } button { - class: if deleting { "text-xs text-paper-secondary cursor-not-allowed" } else { BTN_TEXT_RED }, + class: if deleting { + "relative inline-flex items-center text-xs text-paper-secondary cursor-not-allowed" + } else { + BTN_TEXT_RED + }, disabled: deleting, onclick: move |_| on_delete.call(post.id), - if deleting { - "删除中..." - } else { + span { + class: if deleting { "opacity-40" } else { "" }, "删除" } + if deleting { + span { + class: "absolute inset-0 flex items-center justify-center", + dangerous_inner_html: SPINNER_SVG, + } + } } } }