diff --git a/src/pages/admin/dashboard.rs b/src/pages/admin/dashboard.rs index e1bd421..54f27c1 100644 --- a/src/pages/admin/dashboard.rs +++ b/src/pages/admin/dashboard.rs @@ -1,42 +1,19 @@ use dioxus::prelude::*; use crate::api::posts::{get_post_stats, list_posts, PostListResponse, PostStatsResponse}; -use crate::hooks::delayed_loading::use_delayed_loading; +use crate::components::suspense_wrapper::SuspenseWrapper; use crate::models::post::Post; #[component] pub fn Admin() -> Element { - let stats_res = use_resource(get_post_stats); - let posts_res = use_resource(list_posts); - let show_stats_skeleton = use_delayed_loading(move || stats_res.read().is_none()); - let show_posts_skeleton = use_delayed_loading(move || posts_res.read().is_none()); - rsx! { div { class: "space-y-8", - // 统计卡片 div { class: "grid grid-cols-1 md:grid-cols-3 gap-6", - match &*stats_res.read() { - Some(Ok(PostStatsResponse { stats })) => { - rsx! { - StatCard { value: stats.total.to_string(), label: "文章总数" } - StatCard { value: stats.drafts.to_string(), label: "草稿数" } - StatCard { value: stats.published.to_string(), label: "已发布" } - } - } - _ => { - rsx! { - for _ in 0..3 { - div { class: if show_stats_skeleton() { "rounded-xl bg-white dark:bg-[#2e2e33] border border-gray-200 dark:border-[#333] p-6 text-center space-y-3 animate-pulse" } else { "rounded-xl bg-white dark:bg-[#2e2e33] border border-gray-200 dark:border-[#333] p-6 text-center space-y-3 opacity-0" }, - div { class: "h-9 w-16 mx-auto bg-gray-200 dark:bg-[#2a2a2a] rounded" } - div { class: "h-4 w-20 mx-auto bg-gray-200 dark:bg-[#2a2a2a] rounded" } - } - } - } - } + SuspenseWrapper { + StatsSection {} } } - // 快捷操作 div { class: "grid grid-cols-1 md:grid-cols-2 gap-4", button { class: "bg-gray-900 dark:bg-[#dadadb] text-white dark:text-gray-900 rounded-full px-6 py-3 text-center font-medium hover:opacity-80 transition-opacity cursor-pointer", @@ -54,36 +31,85 @@ pub fn Admin() -> Element { } } - // 最近文章 div { class: "mb-8", h2 { class: "text-xl font-bold text-gray-900 dark:text-[#dadadb] mb-4", "最近文章" } - match &*posts_res.read() { - Some(Ok(PostListResponse { posts })) => { - rsx! { - div { class: "space-y-0", - for post in posts.iter().take(5) { - RecentPostItem { post: post.clone() } - } - } - } - } - _ => { - rsx! { - div { class: if show_posts_skeleton() { "space-y-4 animate-pulse" } else { "space-y-4 opacity-0" }, - for _ in 0..5 { - div { class: "flex justify-between items-center py-3 border-b border-gray-100 dark:border-[#333]", - div { class: "h-4 w-[45%] bg-gray-200 dark:bg-[#2a2a2a] rounded" } - div { class: "h-3 w-20 bg-gray-200 dark:bg-[#2a2a2a] rounded" } - } - } - } - } + SuspenseWrapper { + RecentPostsSection {} + } + } + } + } +} + +#[component] +fn StatsSection() -> Element { + let stats_res = use_server_future(get_post_stats)?; + + let stats_data = stats_res.read().as_ref().map(|r| match r { + Ok(PostStatsResponse { stats }) => Ok(stats.clone()), + Err(_) => Err(()), + }); + + match stats_data { + Some(Ok(stats)) => { + rsx! { + StatCard { value: stats.total.to_string(), label: "文章总数" } + StatCard { value: stats.drafts.to_string(), label: "草稿数" } + StatCard { value: stats.published.to_string(), label: "已发布" } + } + } + Some(Err(_)) => { + rsx! { + div { class: "col-span-3 text-center text-red-500 dark:text-red-400 py-6", + "加载统计失败" + } + } + } + _ => { + rsx! { + div { class: "col-span-3 text-center text-gray-500 dark:text-[#9b9c9d] py-6", + "加载中..." + } + } + } + } +} + +#[component] +fn RecentPostsSection() -> Element { + let posts_res = use_server_future(list_posts)?; + + let posts_data = posts_res.read().as_ref().map(|r| match r { + Ok(PostListResponse { posts }) => Ok(posts.clone()), + Err(_) => Err(()), + }); + + match posts_data { + Some(Ok(posts)) => { + rsx! { + div { class: "space-y-0", + for post in posts.iter().take(5) { + RecentPostItem { post: post.clone() } } } } } + Some(Err(_)) => { + rsx! { + div { class: "text-center text-red-500 dark:text-red-400 py-6", + "加载文章列表失败" + } + } + } + _ => { + rsx! { + div { class: "text-center text-gray-500 dark:text-[#9b9c9d] py-6", + "加载中..." + } + } + } } } diff --git a/src/pages/admin/posts.rs b/src/pages/admin/posts.rs index df26ef7..4b24fd8 100644 --- a/src/pages/admin/posts.rs +++ b/src/pages/admin/posts.rs @@ -1,15 +1,11 @@ use dioxus::prelude::*; use crate::api::posts::{delete_post, list_posts, CreatePostResponse, PostListResponse}; -use crate::hooks::delayed_loading::use_delayed_loading; +use crate::components::suspense_wrapper::SuspenseWrapper; use crate::models::post::{Post, PostStatus}; #[component] pub fn Posts() -> Element { - let mut posts_res = use_resource(list_posts); - let mut deleting = use_signal(|| None::); - let show_skeleton = use_delayed_loading(move || posts_res.read().is_none()); - rsx! { div { class: "space-y-6", div { class: "flex items-center justify-between", @@ -24,52 +20,70 @@ pub fn Posts() -> Element { "+ 写文章" } } + SuspenseWrapper { + PostsTable {} + } + } + } +} - match &*posts_res.read() { - Some(Ok(PostListResponse { posts })) => { - if posts.is_empty() { - rsx! { - div { class: "text-center py-20 text-gray-500 dark:text-[#9b9c9d]", - "暂无文章" +#[component] +fn PostsTable() -> Element { + let mut refresh = use_signal(|| 0); + let mut deleting = use_signal(|| None::); + let posts_res = use_server_future(move || { + let _ = refresh(); + list_posts() + })?; + + let posts_data = posts_res.read().as_ref().map(|r| match r { + Ok(PostListResponse { posts }) => Ok(posts.clone()), + Err(e) => Err(e.to_string()), + }); + + match posts_data { + Some(Ok(posts)) => { + if posts.is_empty() { + rsx! { + div { class: "text-center py-20 text-gray-500 dark:text-[#9b9c9d]", + "暂无文章" + } + } + } else { + rsx! { + div { class: "bg-white dark:bg-[#2e2e33] rounded-xl border border-gray-200 dark:border-[#333] overflow-hidden", + 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", "标题" } + th { class: "px-4 py-3 font-medium w-24 text-center", "状态" } + th { class: "px-4 py-3 font-medium w-32", "日期" } + th { class: "px-4 py-3 font-medium w-24 text-right", "操作" } + } } - } - } else { - rsx! { - div { class: "bg-white dark:bg-[#2e2e33] rounded-xl border border-gray-200 dark:border-[#333] overflow-hidden", - 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", "标题" } - th { class: "px-4 py-3 font-medium w-24 text-center", "状态" } - th { class: "px-4 py-3 font-medium w-32", "日期" } - th { class: "px-4 py-3 font-medium w-24 text-right", "操作" } - } - } - tbody { - for post in posts.iter() { - PostRow { - post: post.clone(), - deleting: deleting() == Some(post.id), - on_delete: move |id| { - deleting.set(Some(id)); - spawn(async move { - match delete_post(id).await { - Ok(CreatePostResponse { success: true, .. }) => { - posts_res.restart(); - } - Ok(CreatePostResponse { success: false, message, .. }) => { - #[cfg(target_arch = "wasm32")] - web_sys::window().map(|w| w.alert_with_message(&message).ok()); - } - Err(e) => { - #[cfg(target_arch = "wasm32")] - web_sys::window().map(|w| w.alert_with_message(&format!("删除失败: {}", e)).ok()); - } - } - deleting.set(None); - }); + tbody { + for post in posts.iter() { + PostRow { + post: post.clone(), + deleting: deleting() == Some(post.id), + on_delete: move |id| { + deleting.set(Some(id)); + spawn(async move { + match delete_post(id).await { + Ok(CreatePostResponse { success: true, .. }) => { + refresh.set(refresh() + 1); + } + Ok(CreatePostResponse { success: false, message, .. }) => { + #[cfg(target_arch = "wasm32")] + web_sys::window().map(|w| w.alert_with_message(&message).ok()); + } + Err(e) => { + #[cfg(target_arch = "wasm32")] + web_sys::window().map(|w| w.alert_with_message(&format!("删除失败: {}", e)).ok()); + } } - } + deleting.set(None); + }); } } } @@ -77,24 +91,19 @@ pub fn Posts() -> Element { } } } - Some(Err(e)) => { - rsx! { - div { class: "text-center text-red-500 dark:text-red-400 py-20", - "加载失败: {e}" - } - } + } + } + Some(Err(e)) => { + rsx! { + div { class: "text-center text-red-500 dark:text-red-400 py-20", + "加载失败: {e}" } - None => { - rsx! { - div { class: if show_skeleton() { "bg-white dark:bg-[#2e2e33] rounded-xl border border-gray-200 dark:border-[#333] animate-pulse" } else { "bg-white dark:bg-[#2e2e33] rounded-xl border border-gray-200 dark:border-[#333] opacity-0" }, - for _ in 0..5 { - div { class: "flex items-center px-4 py-3 border-b border-gray-100 dark:border-[#333] last:border-0", - div { class: "h-4 w-1/3 bg-gray-200 dark:bg-[#2a2a2a] rounded" } - div { class: "ml-auto h-4 w-16 bg-gray-200 dark:bg-[#2a2a2a] rounded" } - } - } - } - } + } + } + _ => { + rsx! { + div { class: "text-center text-gray-500 dark:text-[#9b9c9d] py-20", + "加载中..." } } }