fix(admin/posts): 重建支持并发 loading,spinner 覆盖文字

- rebuilding 信号由 Option<i32> 改为 HashSet<i32>,连续点击多篇
  文章的重建按钮可同时处于 loading 态(单值会被后点的覆盖)
- loading 样式调整:按钮文案恒为「重建」,spinner 绝对定位覆盖在
  文字上方居中,文字 opacity 调至 40% 衬底,不再切换文案
This commit is contained in:
xfy 2026-07-03 10:40:11 +08:00
parent a5c00caa80
commit db87d3c886

View File

@ -59,8 +59,9 @@ pub fn PostsPage(page: i32) -> Element {
// 删除中 ID、重建缓存状态与结果仍由本组件持有业务逻辑不归 hook 管)。 // 删除中 ID、重建缓存状态与结果仍由本组件持有业务逻辑不归 hook 管)。
let mut deleting = use_signal(|| None::<i32>); let mut deleting = use_signal(|| None::<i32>);
// 单篇重建中 ID与 deleting 同形,按行禁用按钮并切换文案。 // 重建中文章 ID 集合:支持多篇文章并发重建(行不会随点击消失,单值会被后点
let mut rebuilding = use_signal(|| None::<i32>); // 的覆盖先点的,故用 HashSet按行通过 contains 判断 loading 态。
let mut rebuilding = use_signal(|| std::collections::HashSet::<i32>::new());
// 重建缓存的状态由本组件持有并下发给 RebuildCacheBar结果消息也在本组件 // 重建缓存的状态由本组件持有并下发给 RebuildCacheBar结果消息也在本组件
// 渲染header 与表格之间的独立行),既不撑高 header 触发 items-center 重排, // 渲染header 与表格之间的独立行),既不撑高 header 触发 items-center 重排,
// 也不脱离文档流溢进表格。rebuilding 仅按钮态用,不在此渲染。 // 也不脱离文档流溢进表格。rebuilding 仅按钮态用,不在此渲染。
@ -130,7 +131,7 @@ pub fn PostsPage(page: i32) -> Element {
key: "{post.id}", key: "{post.id}",
post: post.clone(), post: post.clone(),
deleting: deleting() == Some(post.id), deleting: deleting() == Some(post.id),
rebuilding: rebuilding() == Some(post.id), rebuilding: rebuilding().contains(&post.id),
// 删除文章:先乐观更新本地列表,再调用 server function失败时弹出浏览器提示。 // 删除文章:先乐观更新本地列表,再调用 server function失败时弹出浏览器提示。
on_delete: move |id| { on_delete: move |id| {
deleting.set(Some(id)); deleting.set(Some(id));
@ -154,11 +155,12 @@ pub fn PostsPage(page: i32) -> Element {
}, },
// 重建单篇文章内容:调用 server function 重新渲染 content_html。 // 重建单篇文章内容:调用 server function 重新渲染 content_html。
// 静默执行,仅按行切换 rebuilding 按钮态,不弹窗。 // 静默执行,仅按行切换 rebuilding 按钮态,不弹窗。
// 用 HashSet 记录在途 ID支持多篇并发重建。
on_rebuild: move |id| { on_rebuild: move |id| {
rebuilding.set(Some(id)); rebuilding.write().insert(id);
spawn(async move { spawn(async move {
let _ = rebuild_post_content_html(id).await; let _ = rebuild_post_content_html(id).await;
rebuilding.set(None); rebuilding.write().remove(&id);
}); });
}, },
} }
@ -304,20 +306,22 @@ fn PostRow(
} }
button { button {
class: if rebuilding { 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 { } else {
BTN_TEXT_ACCENT BTN_TEXT_ACCENT
}, },
disabled: rebuilding, disabled: rebuilding,
onclick: move |_| on_rebuild.call(post.id), onclick: move |_| on_rebuild.call(post.id),
if rebuilding { span {
{rsx! { class: if rebuilding { "opacity-40" } else { "" },
span { dangerous_inner_html: SPINNER_SVG }
"重建中"
}}
} else {
"重建" "重建"
} }
if rebuilding {
span {
class: "absolute inset-0 flex items-center justify-center",
dangerous_inner_html: SPINNER_SVG,
}
}
} }
button { button {
class: if deleting { "text-xs text-paper-secondary cursor-not-allowed" } else { BTN_TEXT_RED }, class: if deleting { "text-xs text-paper-secondary cursor-not-allowed" } else { BTN_TEXT_RED },