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,34 +4,43 @@ 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 }
}
}
}
}
#[component]
fn HomePosts(current_page: i32) -> Element {
let posts_res = use_server_future(move || list_published_posts(current_page, POSTS_PER_PAGE))?;
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! { rsx! {
for post in posts.iter() { for post in posts.iter() {
PostCard { post: post.clone() } PostCard { post: post.clone() }
@ -51,18 +60,10 @@ fn HomeContent(page: i32) -> Element {
} }
} }
} }
None => { _ => {
rsx! { rsx! {
div { class: if show_skeleton() { "space-y-6 py-4" } else { "space-y-6 py-4 opacity-0" }, div { class: "text-center text-gray-500 dark:text-[#9b9c9d] py-20",
for _ in 0..3 { "加载中..."
div { class: "mb-6 p-6 bg-white dark:bg-[#2e2e33] rounded-lg border border-gray-200 dark:border-[#333] animate-pulse",
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,8 +18,25 @@ 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 {
TagsContent {}
}
}
}
}
#[component]
fn TagsContent() -> Element {
let tags_res = use_server_future(list_tags)?;
let tags_data = tags_res.read().as_ref().map(|r| match r {
Ok(TagListResponse { tags }) => Ok(tags.clone()),
Err(e) => Err(e.to_string()),
});
match tags_data {
Some(Ok(tags)) => {
let total = tags.iter().map(|t| t.post_count).sum::<i64>(); let total = tags.iter().map(|t| t.post_count).sum::<i64>();
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]",
@ -31,30 +46,8 @@ pub fn Tags() -> Element {
span { class: "font-medium text-gray-700 dark:text-[#dadadb]", "{total}" } span { class: "font-medium text-gray-700 dark:text-[#dadadb]", "{total}" }
" 篇文章" " 篇文章"
} }
}
}
Some(Err(_)) => {
rsx! {
div { class: "mt-2 text-base text-gray-500 dark:text-[#9b9c9d]",
"加载失败"
}
}
}
None => {
rsx! {
div { class: "mt-2 text-base text-gray-500 dark:text-[#9b9c9d]",
"加载中..."
}
}
}
}
}
match &*tags_res.read() {
Some(Ok(TagListResponse { tags })) => {
let tags = tags.clone();
rsx! {
ul { class: "flex flex-wrap gap-4 mt-6", ul { class: "flex flex-wrap gap-4 mt-6",
for tag in tags.into_iter() { for tag in tags {
li { li {
a { 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", 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",
@ -78,14 +71,10 @@ pub fn Tags() -> Element {
} }
} }
} }
None => { _ => {
rsx! { 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" }, div { class: "text-center text-gray-500 dark:text-[#9b9c9d] py-20",
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,35 +92,31 @@ 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 {
TagDetailContent { tag: tag.clone() }
}
}
}
}
#[component]
fn TagDetailContent(tag: String) -> Element {
let posts_res = use_server_future(move || get_posts_by_tag(tag.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! { 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]", "{posts.len()}" } span { class: "font-medium text-gray-700 dark:text-[#dadadb]", "{posts.len()}" }
" 篇文章" " 篇文章"
} }
}
}
Some(Err(_)) => {
rsx! {
div { class: "mt-2 text-base text-gray-500 dark:text-[#9b9c9d]",
"加载失败"
}
}
}
None => {
rsx! {
div { class: "mt-2 text-base text-gray-500 dark:text-[#9b9c9d]",
"加载中..."
}
}
}
}
}
match &*posts_res.read() {
Some(Ok(PostListResponse { posts })) => {
rsx! {
for post in posts.iter() { for post in posts.iter() {
PostCard { post: post.clone() } PostCard { post: post.clone() }
} }
@ -147,18 +129,10 @@ pub fn TagDetail(tag: String) -> Element {
} }
} }
} }
None => { _ => {
rsx! { rsx! {
div { class: if show_skeleton() { "space-y-6 py-4 animate-pulse" } else { "space-y-6 py-4 opacity-0" }, div { class: "text-center text-gray-500 dark:text-[#9b9c9d] py-20",
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" }
}
}
}
}
} }
} }
} }