feat: SSR for admin pages
This commit is contained in:
parent
1afb477829
commit
34e70f6c57
@ -1,42 +1,19 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
use crate::api::posts::{get_post_stats, list_posts, PostListResponse, PostStatsResponse};
|
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;
|
use crate::models::post::Post;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Admin() -> Element {
|
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! {
|
rsx! {
|
||||||
div { class: "space-y-8",
|
div { class: "space-y-8",
|
||||||
// 统计卡片
|
|
||||||
div { class: "grid grid-cols-1 md:grid-cols-3 gap-6",
|
div { class: "grid grid-cols-1 md:grid-cols-3 gap-6",
|
||||||
match &*stats_res.read() {
|
SuspenseWrapper {
|
||||||
Some(Ok(PostStatsResponse { stats })) => {
|
StatsSection {}
|
||||||
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" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 快捷操作
|
|
||||||
div { class: "grid grid-cols-1 md:grid-cols-2 gap-4",
|
div { class: "grid grid-cols-1 md:grid-cols-2 gap-4",
|
||||||
button {
|
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",
|
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,13 +31,63 @@ pub fn Admin() -> Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 最近文章
|
|
||||||
div { class: "mb-8",
|
div { class: "mb-8",
|
||||||
h2 { class: "text-xl font-bold text-gray-900 dark:text-[#dadadb] mb-4",
|
h2 { class: "text-xl font-bold text-gray-900 dark:text-[#dadadb] mb-4",
|
||||||
"最近文章"
|
"最近文章"
|
||||||
}
|
}
|
||||||
match &*posts_res.read() {
|
SuspenseWrapper {
|
||||||
Some(Ok(PostListResponse { posts })) => {
|
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! {
|
rsx! {
|
||||||
div { class: "space-y-0",
|
div { class: "space-y-0",
|
||||||
for post in posts.iter().take(5) {
|
for post in posts.iter().take(5) {
|
||||||
@ -69,18 +96,17 @@ pub fn Admin() -> Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Some(Err(_)) => {
|
||||||
|
rsx! {
|
||||||
|
div { class: "text-center text-red-500 dark:text-red-400 py-6",
|
||||||
|
"加载文章列表失败"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
rsx! {
|
rsx! {
|
||||||
div { class: if show_posts_skeleton() { "space-y-4 animate-pulse" } else { "space-y-4 opacity-0" },
|
div { class: "text-center text-gray-500 dark:text-[#9b9c9d] py-6",
|
||||||
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" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +1,11 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
use crate::api::posts::{delete_post, list_posts, CreatePostResponse, PostListResponse};
|
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};
|
use crate::models::post::{Post, PostStatus};
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Posts() -> Element {
|
pub fn Posts() -> Element {
|
||||||
let mut posts_res = use_resource(list_posts);
|
|
||||||
let mut deleting = use_signal(|| None::<i32>);
|
|
||||||
let show_skeleton = use_delayed_loading(move || posts_res.read().is_none());
|
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
div { class: "space-y-6",
|
div { class: "space-y-6",
|
||||||
div { class: "flex items-center justify-between",
|
div { class: "flex items-center justify-between",
|
||||||
@ -24,9 +20,29 @@ pub fn Posts() -> Element {
|
|||||||
"+ 写文章"
|
"+ 写文章"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
SuspenseWrapper {
|
||||||
|
PostsTable {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
match &*posts_res.read() {
|
#[component]
|
||||||
Some(Ok(PostListResponse { posts })) => {
|
fn PostsTable() -> Element {
|
||||||
|
let mut refresh = use_signal(|| 0);
|
||||||
|
let mut deleting = use_signal(|| None::<i32>);
|
||||||
|
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() {
|
if posts.is_empty() {
|
||||||
rsx! {
|
rsx! {
|
||||||
div { class: "text-center py-20 text-gray-500 dark:text-[#9b9c9d]",
|
div { class: "text-center py-20 text-gray-500 dark:text-[#9b9c9d]",
|
||||||
@ -55,7 +71,7 @@ pub fn Posts() -> Element {
|
|||||||
spawn(async move {
|
spawn(async move {
|
||||||
match delete_post(id).await {
|
match delete_post(id).await {
|
||||||
Ok(CreatePostResponse { success: true, .. }) => {
|
Ok(CreatePostResponse { success: true, .. }) => {
|
||||||
posts_res.restart();
|
refresh.set(refresh() + 1);
|
||||||
}
|
}
|
||||||
Ok(CreatePostResponse { success: false, message, .. }) => {
|
Ok(CreatePostResponse { success: false, message, .. }) => {
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
@ -84,17 +100,10 @@ pub fn Posts() -> Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
_ => {
|
||||||
rsx! {
|
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" },
|
div { class: "text-center text-gray-500 dark:text-[#9b9c9d] py-20",
|
||||||
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" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user