fix(admin): 评论管理页改为 CSR 避免按钮点击闪烁
This commit is contained in:
parent
569eec5bf8
commit
ea440cd61c
@ -4,13 +4,14 @@ use dioxus::prelude::*;
|
|||||||
use dioxus::router::components::Link;
|
use dioxus::router::components::Link;
|
||||||
|
|
||||||
use crate::api::comments::{
|
use crate::api::comments::{
|
||||||
approve_comment, batch_update_comment_status, get_all_comments, spam_comment,
|
approve_comment, batch_update_comment_status, spam_comment,
|
||||||
AllCommentsResponse,
|
|
||||||
};
|
};
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
use crate::api::comments::{get_all_comments, AllCommentsResponse};
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
use crate::api::comments::trash_comment;
|
use crate::api::comments::trash_comment;
|
||||||
use crate::components::skeletons::delayed_skeleton::DelayedSkeleton;
|
use crate::components::skeletons::delayed_skeleton::DelayedSkeleton;
|
||||||
use crate::models::comment::CommentStatus;
|
use crate::models::comment::{AdminComment, CommentStatus};
|
||||||
use crate::router::Route;
|
use crate::router::Route;
|
||||||
|
|
||||||
const COMMENTS_PER_PAGE: i32 = 20;
|
const COMMENTS_PER_PAGE: i32 = 20;
|
||||||
@ -43,12 +44,60 @@ pub fn AdminCommentsPage(page: i32) -> Element {
|
|||||||
String::new()
|
String::new()
|
||||||
});
|
});
|
||||||
let mut selected_ids: Signal<HashSet<i64>> = use_signal(HashSet::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 filter_status = move || {
|
||||||
let f = active_filter();
|
let f = active_filter();
|
||||||
if f.is_empty() { None } else { Some(f) }
|
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! {
|
rsx! {
|
||||||
div { class: "space-y-6",
|
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",
|
class: "px-3 py-1.5 text-xs font-medium bg-green-600 text-white rounded hover:bg-green-700 transition-colors",
|
||||||
onclick: move |_| {
|
onclick: move |_| {
|
||||||
let ids: Vec<i64> = selected_ids().iter().copied().collect();
|
let ids: Vec<i64> = selected_ids().iter().copied().collect();
|
||||||
|
let ids_for_api = ids.clone();
|
||||||
spawn(async move {
|
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());
|
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",
|
class: "px-3 py-1.5 text-xs font-medium bg-amber-600 text-white rounded hover:bg-amber-700 transition-colors",
|
||||||
onclick: move |_| {
|
onclick: move |_| {
|
||||||
let ids: Vec<i64> = selected_ids().iter().copied().collect();
|
let ids: Vec<i64> = selected_ids().iter().copied().collect();
|
||||||
|
let ids_for_api = ids.clone();
|
||||||
spawn(async move {
|
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());
|
selected_ids.set(HashSet::new());
|
||||||
comments_res.restart();
|
|
||||||
},
|
},
|
||||||
"批量垃圾"
|
"批量垃圾"
|
||||||
}
|
}
|
||||||
@ -110,11 +161,12 @@ pub fn AdminCommentsPage(page: i32) -> Element {
|
|||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
{
|
{
|
||||||
let ids: Vec<i64> = selected_ids().iter().copied().collect();
|
let ids: Vec<i64> = selected_ids().iter().copied().collect();
|
||||||
|
let ids_for_api = ids.clone();
|
||||||
spawn(async move {
|
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());
|
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 {
|
if error().is_some() {
|
||||||
Ok(AllCommentsResponse { comments, total }) => Ok((comments.clone(), *total)),
|
rsx! {
|
||||||
Err(e) => Err(e.to_string()),
|
div { class: "text-center text-red-500 dark:text-red-400 py-20",
|
||||||
});
|
"加载失败"
|
||||||
match data {
|
}
|
||||||
Some(Ok((comments, total))) => {
|
}
|
||||||
if comments.is_empty() {
|
} else if loading() && comments().is_empty() {
|
||||||
rsx! {
|
rsx! {
|
||||||
div { class: "text-center py-20 text-gray-500 dark:text-[#9b9c9d]",
|
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();
|
} else if comments().is_empty() {
|
||||||
rsx! {
|
rsx! {
|
||||||
div { class: "bg-white dark:bg-[#2e2e33] rounded-xl border border-gray-200 dark:border-[#333] overflow-hidden",
|
div { class: "text-center py-20 text-gray-500 dark:text-[#9b9c9d]",
|
||||||
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]",
|
} else {
|
||||||
th { class: "px-4 py-3 font-medium w-10",
|
let list = comments();
|
||||||
input {
|
let all_selected = list.iter().all(|c| selected_ids().contains(&c.id));
|
||||||
r#type: "checkbox",
|
let all_ids: Vec<i64> = list.iter().map(|c| c.id).collect();
|
||||||
class: "rounded border-gray-300 dark:border-[#555]",
|
rsx! {
|
||||||
checked: all_selected,
|
div { class: "bg-white dark:bg-[#2e2e33] rounded-xl border border-gray-200 dark:border-[#333] overflow-hidden",
|
||||||
onchange: {
|
div { class: "overflow-x-auto",
|
||||||
move |_| {
|
table { class: "w-full text-sm",
|
||||||
let mut s = selected_ids();
|
thead {
|
||||||
if all_selected {
|
tr { class: "border-b border-gray-200 dark:border-[#333] text-left text-gray-500 dark:text-[#9b9c9d]",
|
||||||
for id in &all_ids { s.remove(id); }
|
th { class: "px-4 py-3 font-medium w-10",
|
||||||
} else {
|
input {
|
||||||
for id in &all_ids { s.insert(*id); }
|
r#type: "checkbox",
|
||||||
}
|
class: "rounded border-gray-300 dark:border-[#555]",
|
||||||
selected_ids.set(s);
|
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]
|
#[component]
|
||||||
fn CommentRow(
|
fn CommentRow(
|
||||||
comment: crate::models::comment::AdminComment,
|
comment: AdminComment,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
on_select: EventHandler<bool>,
|
on_select: EventHandler<bool>,
|
||||||
on_approve: EventHandler,
|
on_approve: EventHandler,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user