From ea440cd61c9881290772a4e19a3325560b2aedd6 Mon Sep 17 00:00:00 2001 From: xfy Date: Thu, 11 Jun 2026 17:34:39 +0800 Subject: [PATCH] =?UTF-8?q?fix(admin):=20=E8=AF=84=E8=AE=BA=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E9=A1=B5=E6=94=B9=E4=B8=BA=20CSR=20=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E6=8C=89=E9=92=AE=E7=82=B9=E5=87=BB=E9=97=AA=E7=83=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/admin/comments.rs | 306 ++++++++++++++++++++---------------- 1 file changed, 172 insertions(+), 134 deletions(-) diff --git a/src/pages/admin/comments.rs b/src/pages/admin/comments.rs index a134a78..f78fd46 100644 --- a/src/pages/admin/comments.rs +++ b/src/pages/admin/comments.rs @@ -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> = use_signal(HashSet::new); + let mut comments: Signal> = use_signal(Vec::new); + let mut total: Signal = use_signal(|| 0); + #[allow(unused_mut)] + let mut loading: Signal = use_signal(|| false); + #[allow(unused_mut)] + let mut error: Signal> = 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 = 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 = 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 = 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 = 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 = 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, on_approve: EventHandler,