fix(admin): 评论管理页改为 CSR 避免按钮点击闪烁

This commit is contained in:
xfy 2026-06-11 17:34:39 +08:00
parent 569eec5bf8
commit ea440cd61c

View File

@ -4,13 +4,14 @@ use dioxus::prelude::*;
use dioxus::router::components::Link;
use crate::api::comments::{
approve_comment, batch_update_comment_status, get_all_comments, spam_comment,
AllCommentsResponse,
approve_comment, batch_update_comment_status, spam_comment,
};
#[cfg(target_arch = "wasm32")]
use crate::api::comments::{get_all_comments, AllCommentsResponse};
#[cfg(target_arch = "wasm32")]
use crate::api::comments::trash_comment;
use crate::components::skeletons::delayed_skeleton::DelayedSkeleton;
use crate::models::comment::CommentStatus;
use crate::models::comment::{AdminComment, CommentStatus};
use crate::router::Route;
const COMMENTS_PER_PAGE: i32 = 20;
@ -43,12 +44,60 @@ pub fn AdminCommentsPage(page: i32) -> Element {
String::new()
});
let mut selected_ids: Signal<HashSet<i64>> = use_signal(HashSet::new);
let mut comments: Signal<Vec<AdminComment>> = use_signal(Vec::new);
let mut total: Signal<i64> = use_signal(|| 0);
#[allow(unused_mut)]
let mut loading: Signal<bool> = use_signal(|| false);
#[allow(unused_mut)]
let mut error: Signal<Option<String>> = use_signal(|| None);
#[allow(unused_variables)]
let filter_status = move || {
let f = active_filter();
if f.is_empty() { None } else { Some(f) }
};
let mut comments_res =
use_server_future(move || get_all_comments(current_page, filter_status()))?;
// 客户端CSR加载数据
use_effect(move || {
let _ = active_filter();
let _ = current_page;
#[cfg(target_arch = "wasm32")]
{
let page = current_page;
let status = filter_status();
spawn(async move {
loading.set(true);
error.set(None);
match get_all_comments(page, status).await {
Ok(AllCommentsResponse {
comments: list,
total: t,
}) => {
comments.set(list);
total.set(t);
}
Err(e) => error.set(Some(e.to_string())),
}
loading.set(false);
});
}
});
#[allow(unused_mut)]
let mut set_comment_status = move |id: i64, status: CommentStatus| {
comments.with_mut(|list| {
if let Some(c) = list.iter_mut().find(|c| c.id == id) {
c.status = status;
}
});
};
#[allow(unused_mut, unused_variables)]
let mut remove_comment = move |id: i64| {
comments.with_mut(|list| list.retain(|c| c.id != id));
total.with_mut(|t| *t = t.saturating_sub(1));
};
rsx! {
div { class: "space-y-6",
@ -80,11 +129,12 @@ pub fn AdminCommentsPage(page: i32) -> Element {
class: "px-3 py-1.5 text-xs font-medium bg-green-600 text-white rounded hover:bg-green-700 transition-colors",
onclick: move |_| {
let ids: Vec<i64> = selected_ids().iter().copied().collect();
let ids_for_api = ids.clone();
spawn(async move {
let _ = batch_update_comment_status(ids, "approved".to_string()).await;
let _ = batch_update_comment_status(ids_for_api, "approved".to_string()).await;
});
for id in &ids { set_comment_status(*id, CommentStatus::Approved); }
selected_ids.set(HashSet::new());
comments_res.restart();
},
"批量通过"
}
@ -92,11 +142,12 @@ pub fn AdminCommentsPage(page: i32) -> Element {
class: "px-3 py-1.5 text-xs font-medium bg-amber-600 text-white rounded hover:bg-amber-700 transition-colors",
onclick: move |_| {
let ids: Vec<i64> = selected_ids().iter().copied().collect();
let ids_for_api = ids.clone();
spawn(async move {
let _ = batch_update_comment_status(ids, "spam".to_string()).await;
let _ = batch_update_comment_status(ids_for_api, "spam".to_string()).await;
});
for id in &ids { set_comment_status(*id, CommentStatus::Spam); }
selected_ids.set(HashSet::new());
comments_res.restart();
},
"批量垃圾"
}
@ -110,11 +161,12 @@ pub fn AdminCommentsPage(page: i32) -> Element {
.unwrap_or(false)
{
let ids: Vec<i64> = selected_ids().iter().copied().collect();
let ids_for_api = ids.clone();
spawn(async move {
let _ = batch_update_comment_status(ids, "trash".to_string()).await;
let _ = batch_update_comment_status(ids_for_api, "trash".to_string()).await;
});
for id in &ids { remove_comment(*id); }
selected_ids.set(HashSet::new());
comments_res.restart();
}
}
},
@ -125,139 +177,125 @@ pub fn AdminCommentsPage(page: i32) -> Element {
}
{
let data = comments_res.read().as_ref().map(|r| match r {
Ok(AllCommentsResponse { comments, total }) => Ok((comments.clone(), *total)),
Err(e) => Err(e.to_string()),
});
match data {
Some(Ok((comments, total))) => {
if comments.is_empty() {
rsx! {
div { class: "text-center py-20 text-gray-500 dark:text-[#9b9c9d]",
"暂无评论"
if error().is_some() {
rsx! {
div { class: "text-center text-red-500 dark:text-red-400 py-20",
"加载失败"
}
}
} else if loading() && comments().is_empty() {
rsx! {
DelayedSkeleton {
div { class: "bg-white dark:bg-[#2e2e33] rounded-xl border border-gray-200 dark:border-[#333] p-6 space-y-4",
for _ in 0..5 {
div { class: "flex items-center gap-4",
div { class: "h-4 w-4 bg-gray-200 dark:bg-[#2a2a2a] rounded" }
div { class: "h-8 w-8 bg-gray-200 dark:bg-[#2a2a2a] rounded-full" }
div { class: "h-4 w-32 bg-gray-200 dark:bg-[#2a2a2a] rounded" }
div { class: "h-4 flex-1 bg-gray-200 dark:bg-[#2a2a2a] rounded" }
}
}
}
} else {
let all_selected = comments.iter().all(|c| selected_ids().contains(&c.id));
let all_ids: Vec<i64> = comments.iter().map(|c| c.id).collect();
rsx! {
div { class: "bg-white dark:bg-[#2e2e33] rounded-xl border border-gray-200 dark:border-[#333] overflow-hidden",
div { class: "overflow-x-auto",
table { class: "w-full text-sm",
thead {
tr { class: "border-b border-gray-200 dark:border-[#333] text-left text-gray-500 dark:text-[#9b9c9d]",
th { class: "px-4 py-3 font-medium w-10",
input {
r#type: "checkbox",
class: "rounded border-gray-300 dark:border-[#555]",
checked: all_selected,
onchange: {
move |_| {
let mut s = selected_ids();
if all_selected {
for id in &all_ids { s.remove(id); }
} else {
for id in &all_ids { s.insert(*id); }
}
selected_ids.set(s);
}
}
}
} else if comments().is_empty() {
rsx! {
div { class: "text-center py-20 text-gray-500 dark:text-[#9b9c9d]",
"暂无评论"
}
}
} else {
let list = comments();
let all_selected = list.iter().all(|c| selected_ids().contains(&c.id));
let all_ids: Vec<i64> = list.iter().map(|c| c.id).collect();
rsx! {
div { class: "bg-white dark:bg-[#2e2e33] rounded-xl border border-gray-200 dark:border-[#333] overflow-hidden",
div { class: "overflow-x-auto",
table { class: "w-full text-sm",
thead {
tr { class: "border-b border-gray-200 dark:border-[#333] text-left text-gray-500 dark:text-[#9b9c9d]",
th { class: "px-4 py-3 font-medium w-10",
input {
r#type: "checkbox",
class: "rounded border-gray-300 dark:border-[#555]",
checked: all_selected,
onchange: {
move |_| {
let mut s = selected_ids();
if all_selected {
for id in &all_ids { s.remove(id); }
} else {
for id in &all_ids { s.insert(*id); }
}
selected_ids.set(s);
}
}
}
}
th { class: "px-4 py-3 font-medium", "作者" }
th { class: "px-4 py-3 font-medium", "内容" }
th { class: "px-4 py-3 font-medium", "文章" }
th { class: "px-4 py-3 font-medium text-center", "状态" }
th { class: "px-4 py-3 font-medium w-28", "日期" }
th { class: "px-4 py-3 font-medium w-32 text-right", "操作" }
}
}
tbody {
for comment in list.iter() {
CommentRow {
key: "{comment.id}",
comment: comment.clone(),
selected: selected_ids().contains(&comment.id),
on_select: {
let id = comment.id;
move |checked: bool| {
let mut s = selected_ids();
if checked { s.insert(id); } else { s.remove(&id); }
selected_ids.set(s);
}
},
on_approve: {
let id = comment.id;
move |_| {
spawn(async move {
let _ = approve_comment(id).await;
});
set_comment_status(id, CommentStatus::Approved);
}
},
on_spam: {
let id = comment.id;
move |_| {
spawn(async move {
let _ = spam_comment(id).await;
});
set_comment_status(id, CommentStatus::Spam);
}
},
on_trash: {
let _id = comment.id;
move |_| {
#[cfg(target_arch = "wasm32")]
{
if web_sys::window()
.and_then(|w| w.confirm_with_message("确定要删除这条评论吗?").ok())
.unwrap_or(false)
{
spawn(async move {
let _ = trash_comment(_id).await;
});
remove_comment(_id);
}
}
}
th { class: "px-4 py-3 font-medium", "作者" }
th { class: "px-4 py-3 font-medium", "内容" }
th { class: "px-4 py-3 font-medium", "文章" }
th { class: "px-4 py-3 font-medium text-center", "状态" }
th { class: "px-4 py-3 font-medium w-28", "日期" }
th { class: "px-4 py-3 font-medium w-32 text-right", "操作" }
}
}
tbody {
for comment in comments.iter() {
CommentRow {
key: "{comment.id}",
comment: comment.clone(),
selected: selected_ids().contains(&comment.id),
on_select: {
let id = comment.id;
move |checked: bool| {
let mut s = selected_ids();
if checked { s.insert(id); } else { s.remove(&id); }
selected_ids.set(s);
}
},
on_approve: {
let mut comments_res = comments_res;
let id = comment.id;
move |_| {
spawn(async move {
let _ = approve_comment(id).await;
comments_res.restart();
});
}
},
on_spam: {
let mut comments_res = comments_res;
let id = comment.id;
move |_| {
spawn(async move {
let _ = spam_comment(id).await;
comments_res.restart();
});
}
},
on_trash: {
#[allow(unused_variables)]
let mut comments_res = comments_res;
let _id = comment.id;
move |_| {
#[cfg(target_arch = "wasm32")]
{
if web_sys::window()
.and_then(|w| w.confirm_with_message("确定要删除这条评论吗?").ok())
.unwrap_or(false)
{
let id = _id;
spawn(async move {
let _ = trash_comment(id).await;
comments_res.restart();
});
}
}
}
},
}
}
},
}
}
}
}
CommentsPagination { current_page, total }
}
}
}
Some(Err(_e)) => {
rsx! {
div { class: "text-center text-red-500 dark:text-red-400 py-20",
"加载失败"
}
}
}
None => {
rsx! {
DelayedSkeleton {
div { class: "bg-white dark:bg-[#2e2e33] rounded-xl border border-gray-200 dark:border-[#333] p-6 space-y-4",
for _ in 0..5 {
div { class: "flex items-center gap-4",
div { class: "h-4 w-4 bg-gray-200 dark:bg-[#2a2a2a] rounded" }
div { class: "h-8 w-8 bg-gray-200 dark:bg-[#2a2a2a] rounded-full" }
div { class: "h-4 w-32 bg-gray-200 dark:bg-[#2a2a2a] rounded" }
div { class: "h-4 flex-1 bg-gray-200 dark:bg-[#2a2a2a] rounded" }
}
}
}
}
}
CommentsPagination { current_page, total: total() }
}
}
}
@ -267,7 +305,7 @@ pub fn AdminCommentsPage(page: i32) -> Element {
#[component]
fn CommentRow(
comment: crate::models::comment::AdminComment,
comment: AdminComment,
selected: bool,
on_select: EventHandler<bool>,
on_approve: EventHandler,