feat: SSR for home and tags pages

This commit is contained in:
xfy 2026-06-03 14:40:15 +08:00
parent 6b3e086628
commit 6cfe664085
2 changed files with 131 additions and 156 deletions

View File

@ -4,65 +4,66 @@ use crate::api::posts::{list_published_posts, PostListResponse};
use crate::components::nav::use_nav_items; use crate::components::nav::use_nav_items;
use crate::components::page_layout::PageLayout; use crate::components::page_layout::PageLayout;
use crate::components::post_card::PostCard; use crate::components::post_card::PostCard;
use crate::hooks::delayed_loading::use_delayed_loading; use crate::components::suspense_wrapper::SuspenseWrapper;
use crate::router::Route; use crate::router::Route;
const POSTS_PER_PAGE: i32 = 10; const POSTS_PER_PAGE: i32 = 10;
#[component] #[component]
pub fn Home() -> Element { pub fn Home() -> Element {
rsx! { HomeContent { page: 1 } } rsx! { HomePage { page: 1 } }
} }
#[component] #[component]
pub fn HomePage(page: i32) -> Element { pub fn HomePage(page: i32) -> Element {
rsx! { HomeContent { page } }
}
#[component]
fn HomeContent(page: i32) -> Element {
let route = use_route::<Route>(); let route = use_route::<Route>();
let current_page = page.max(1);
let posts_res = use_resource(move || list_published_posts(current_page, POSTS_PER_PAGE));
let nav_items = use_nav_items(route); let nav_items = use_nav_items(route);
let show_skeleton = use_delayed_loading(move || posts_res.read().is_none()); let current_page = page.max(1);
rsx! { rsx! {
PageLayout { nav_items, PageLayout { nav_items,
HomeInfo {} HomeInfo {}
match &*posts_res.read() { SuspenseWrapper {
Some(Ok(PostListResponse { posts })) => { HomePosts { current_page }
rsx! { }
for post in posts.iter() { }
PostCard { post: post.clone() } }
} }
if posts.is_empty() {
div { class: "text-center text-gray-500 dark:text-[#9b9c9d] py-20", #[component]
"暂无文章" fn HomePosts(current_page: i32) -> Element {
} let posts_res = use_server_future(move || list_published_posts(current_page, POSTS_PER_PAGE))?;
}
Pagination { current_page, posts: posts.clone() } 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)) => {
rsx! {
for post in posts.iter() {
PostCard { post: post.clone() }
}
if posts.is_empty() {
div { class: "text-center text-gray-500 dark:text-[#9b9c9d] py-20",
"暂无文章"
} }
} }
Some(Err(e)) => { Pagination { current_page, posts: posts.clone() }
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() { "space-y-6 py-4" } else { "space-y-6 py-4 opacity-0" }, _ => {
for _ in 0..3 { rsx! {
div { class: "mb-6 p-6 bg-white dark:bg-[#2e2e33] rounded-lg border border-gray-200 dark:border-[#333] animate-pulse", div { class: "text-center text-gray-500 dark:text-[#9b9c9d] py-20",
div { class: "h-7 w-3/4 bg-gray-200 dark:bg-[#2a2a2a] rounded mb-3" } "加载中..."
div { class: "h-4 w-full bg-gray-200 dark:bg-[#2a2a2a] rounded mb-2" }
div { class: "h-4 w-2/3 bg-gray-200 dark:bg-[#2a2a2a] rounded" }
}
}
}
}
} }
} }
} }

View File

@ -4,15 +4,13 @@ use crate::api::posts::{get_posts_by_tag, list_tags, PostListResponse, TagListRe
use crate::components::nav::use_nav_items; use crate::components::nav::use_nav_items;
use crate::components::page_layout::PageLayout; use crate::components::page_layout::PageLayout;
use crate::components::post_card::PostCard; use crate::components::post_card::PostCard;
use crate::hooks::delayed_loading::use_delayed_loading; use crate::components::suspense_wrapper::SuspenseWrapper;
use crate::router::Route; use crate::router::Route;
#[component] #[component]
pub fn Tags() -> Element { pub fn Tags() -> Element {
let route = use_route::<Route>(); let route = use_route::<Route>();
let tags_res = use_resource(list_tags);
let nav_items = use_nav_items(route); let nav_items = use_nav_items(route);
let show_skeleton = use_delayed_loading(move || tags_res.read().is_none());
rsx! { rsx! {
PageLayout { nav_items, PageLayout { nav_items,
@ -20,72 +18,63 @@ pub fn Tags() -> Element {
h1 { class: "text-[34px] font-bold text-gray-900 dark:text-[#dadadb]", h1 { class: "text-[34px] font-bold text-gray-900 dark:text-[#dadadb]",
"标签" "标签"
} }
match &*tags_res.read() { }
Some(Ok(TagListResponse { tags })) => { SuspenseWrapper {
let total = tags.iter().map(|t| t.post_count).sum::<i64>(); TagsContent {}
rsx! { }
div { class: "mt-2 text-base text-gray-500 dark:text-[#9b9c9d]", }
"" }
span { class: "font-medium text-gray-700 dark:text-[#dadadb]", "{tags.len()}" } }
" 个标签,"
span { class: "font-medium text-gray-700 dark:text-[#dadadb]", "{total}" } #[component]
" 篇文章" fn TagsContent() -> Element {
} let tags_res = use_server_future(list_tags)?;
}
} let tags_data = tags_res.read().as_ref().map(|r| match r {
Some(Err(_)) => { Ok(TagListResponse { tags }) => Ok(tags.clone()),
rsx! { Err(e) => Err(e.to_string()),
div { class: "mt-2 text-base text-gray-500 dark:text-[#9b9c9d]", });
"加载失败"
} match tags_data {
} Some(Ok(tags)) => {
} let total = tags.iter().map(|t| t.post_count).sum::<i64>();
None => { rsx! {
rsx! { div { class: "mt-2 text-base text-gray-500 dark:text-[#9b9c9d]",
div { class: "mt-2 text-base text-gray-500 dark:text-[#9b9c9d]", ""
"加载中..." span { class: "font-medium text-gray-700 dark:text-[#dadadb]", "{tags.len()}" }
" 个标签,"
span { class: "font-medium text-gray-700 dark:text-[#dadadb]", "{total}" }
" 篇文章"
}
ul { class: "flex flex-wrap gap-4 mt-6",
for tag in tags {
li {
a {
class: "inline-flex items-center px-3 py-1.5 text-base font-medium bg-gray-100 dark:bg-[#2e2e33] text-gray-700 dark:text-[#9b9c9d] rounded-lg hover:bg-gray-200 dark:hover:bg-[#333] transition-colors",
href: "/tags/{tag.name}",
onclick: move |evt| {
evt.prevent_default();
dioxus::router::navigator().push(format!("/tags/{}", tag.name).as_str());
},
"{tag.name}"
sup { class: "ml-1 text-sm text-gray-500 dark:text-[#9b9c9d]", "{tag.post_count}" }
} }
} }
} }
} }
} }
match &*tags_res.read() { }
Some(Ok(TagListResponse { tags })) => { Some(Err(_)) => {
let tags = tags.clone(); rsx! {
rsx! { div { class: "text-center text-red-500 dark:text-red-400 py-20",
ul { class: "flex flex-wrap gap-4 mt-6", "加载失败"
for tag in tags.into_iter() {
li {
a {
class: "inline-flex items-center px-3 py-1.5 text-base font-medium bg-gray-100 dark:bg-[#2e2e33] text-gray-700 dark:text-[#9b9c9d] rounded-lg hover:bg-gray-200 dark:hover:bg-[#333] transition-colors",
href: "/tags/{tag.name}",
onclick: move |evt| {
evt.prevent_default();
dioxus::router::navigator().push(format!("/tags/{}", tag.name).as_str());
},
"{tag.name}"
sup { class: "ml-1 text-sm text-gray-500 dark:text-[#9b9c9d]", "{tag.post_count}" }
}
}
}
}
}
} }
Some(Err(_)) => { }
rsx! { }
div { class: "text-center text-red-500 dark:text-red-400 py-20", _ => {
"加载失败" rsx! {
} div { class: "text-center text-gray-500 dark:text-[#9b9c9d] py-20",
} "加载中..."
}
None => {
rsx! {
div { class: if show_skeleton() { "flex flex-wrap gap-4 mt-6 animate-pulse" } else { "flex flex-wrap gap-4 mt-6 opacity-0" },
for _ in 0..8 {
div { class: "h-8 w-16 bg-gray-200 dark:bg-[#2a2a2a] rounded-lg" }
}
}
}
} }
} }
} }
@ -95,10 +84,7 @@ pub fn Tags() -> Element {
#[component] #[component]
pub fn TagDetail(tag: String) -> Element { pub fn TagDetail(tag: String) -> Element {
let route = use_route::<Route>(); let route = use_route::<Route>();
let tag_clone = tag.clone();
let posts_res = use_resource(move || get_posts_by_tag(tag_clone.clone()));
let nav_items = use_nav_items(route); let nav_items = use_nav_items(route);
let show_skeleton = use_delayed_loading(move || posts_res.read().is_none());
rsx! { rsx! {
PageLayout { nav_items, PageLayout { nav_items,
@ -106,59 +92,47 @@ pub fn TagDetail(tag: String) -> Element {
h1 { class: "text-[34px] font-bold text-gray-900 dark:text-[#dadadb]", h1 { class: "text-[34px] font-bold text-gray-900 dark:text-[#dadadb]",
"{tag}" "{tag}"
} }
match &*posts_res.read() { }
Some(Ok(PostListResponse { posts })) => { SuspenseWrapper {
rsx! { TagDetailContent { tag: tag.clone() }
div { class: "mt-2 text-base text-gray-500 dark:text-[#9b9c9d]", }
"" }
span { class: "font-medium text-gray-700 dark:text-[#dadadb]", "{posts.len()}" } }
" 篇文章" }
}
} #[component]
} fn TagDetailContent(tag: String) -> Element {
Some(Err(_)) => { let posts_res = use_server_future(move || get_posts_by_tag(tag.clone()))?;
rsx! {
div { class: "mt-2 text-base text-gray-500 dark:text-[#9b9c9d]", let posts_data = posts_res.read().as_ref().map(|r| match r {
"加载失败" Ok(PostListResponse { posts }) => Ok(posts.clone()),
} Err(e) => Err(e.to_string()),
} });
}
None => { match posts_data {
rsx! { Some(Ok(posts)) => {
div { class: "mt-2 text-base text-gray-500 dark:text-[#9b9c9d]", rsx! {
"加载中..." div { class: "mt-2 text-base text-gray-500 dark:text-[#9b9c9d]",
} ""
} span { class: "font-medium text-gray-700 dark:text-[#dadadb]", "{posts.len()}" }
} " 篇文章"
}
for post in posts.iter() {
PostCard { post: post.clone() }
} }
} }
match &*posts_res.read() { }
Some(Ok(PostListResponse { posts })) => { Some(Err(_)) => {
rsx! { rsx! {
for post in posts.iter() { div { class: "text-center text-red-500 dark:text-red-400 py-20",
PostCard { post: post.clone() } "加载失败"
}
}
} }
Some(Err(_)) => { }
rsx! { }
div { class: "text-center text-red-500 dark:text-red-400 py-20", _ => {
"加载失败" rsx! {
} div { class: "text-center text-gray-500 dark:text-[#9b9c9d] py-20",
} "加载中..."
}
None => {
rsx! {
div { class: if show_skeleton() { "space-y-6 py-4 animate-pulse" } else { "space-y-6 py-4 opacity-0" },
for _ in 0..3 {
div { class: "mb-6 p-6 bg-white dark:bg-[#2e2e33] rounded-lg border border-gray-200 dark:border-[#333]",
div { class: "h-7 w-3/4 bg-gray-200 dark:bg-[#2a2a2a] rounded mb-3" }
div { class: "h-4 w-full bg-gray-200 dark:bg-[#2a2a2a] rounded mb-2" }
div { class: "h-4 w-2/3 bg-gray-200 dark:bg-[#2a2a2a] rounded" }
}
}
}
}
} }
} }
} }