From db87d3c88624deb74129fc7b5be9e3b217e18245 Mon Sep 17 00:00:00 2001 From: xfy Date: Fri, 3 Jul 2026 10:40:11 +0800 Subject: [PATCH] =?UTF-8?q?fix(admin/posts):=20=E9=87=8D=E5=BB=BA=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=B9=B6=E5=8F=91=20loading=EF=BC=8Cspinner=20?= =?UTF-8?q?=E8=A6=86=E7=9B=96=E6=96=87=E5=AD=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - rebuilding 信号由 Option 改为 HashSet,连续点击多篇 文章的重建按钮可同时处于 loading 态(单值会被后点的覆盖) - loading 样式调整:按钮文案恒为「重建」,spinner 绝对定位覆盖在 文字上方居中,文字 opacity 调至 40% 衬底,不再切换文案 --- src/pages/admin/posts.rs | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/pages/admin/posts.rs b/src/pages/admin/posts.rs index 3bf3c03..201041d 100644 --- a/src/pages/admin/posts.rs +++ b/src/pages/admin/posts.rs @@ -59,8 +59,9 @@ pub fn PostsPage(page: i32) -> Element { // 删除中 ID、重建缓存状态与结果仍由本组件持有(业务逻辑不归 hook 管)。 let mut deleting = use_signal(|| None::); - // 单篇重建中 ID:与 deleting 同形,按行禁用按钮并切换文案。 - let mut rebuilding = use_signal(|| None::); + // 重建中文章 ID 集合:支持多篇文章并发重建(行不会随点击消失,单值会被后点 + // 的覆盖先点的,故用 HashSet),按行通过 contains 判断 loading 态。 + let mut rebuilding = use_signal(|| std::collections::HashSet::::new()); // 重建缓存的状态由本组件持有并下发给 RebuildCacheBar:结果消息也在本组件 // 渲染(header 与表格之间的独立行),既不撑高 header 触发 items-center 重排, // 也不脱离文档流溢进表格。rebuilding 仅按钮态用,不在此渲染。 @@ -130,7 +131,7 @@ pub fn PostsPage(page: i32) -> Element { key: "{post.id}", post: post.clone(), deleting: deleting() == Some(post.id), - rebuilding: rebuilding() == Some(post.id), + rebuilding: rebuilding().contains(&post.id), // 删除文章:先乐观更新本地列表,再调用 server function,失败时弹出浏览器提示。 on_delete: move |id| { deleting.set(Some(id)); @@ -154,11 +155,12 @@ pub fn PostsPage(page: i32) -> Element { }, // 重建单篇文章内容:调用 server function 重新渲染 content_html。 // 静默执行,仅按行切换 rebuilding 按钮态,不弹窗。 + // 用 HashSet 记录在途 ID,支持多篇并发重建。 on_rebuild: move |id| { - rebuilding.set(Some(id)); + rebuilding.write().insert(id); spawn(async move { let _ = rebuild_post_content_html(id).await; - rebuilding.set(None); + rebuilding.write().remove(&id); }); }, } @@ -304,20 +306,22 @@ fn PostRow( } button { class: if rebuilding { - "inline-flex items-center gap-1 text-xs text-paper-secondary cursor-not-allowed" + "relative inline-flex items-center text-xs text-paper-accent cursor-not-allowed" } else { BTN_TEXT_ACCENT }, disabled: rebuilding, onclick: move |_| on_rebuild.call(post.id), - if rebuilding { - {rsx! { - span { dangerous_inner_html: SPINNER_SVG } - "重建中" - }} - } else { + span { + class: if rebuilding { "opacity-40" } else { "" }, "重建" } + if rebuilding { + span { + class: "absolute inset-0 flex items-center justify-center", + dangerous_inner_html: SPINNER_SVG, + } + } } button { class: if deleting { "text-xs text-paper-secondary cursor-not-allowed" } else { BTN_TEXT_RED },