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::page_layout::PageLayout;
use crate::components::post_card::PostCard;
use crate::hooks::delayed_loading::use_delayed_loading;
use crate::components::suspense_wrapper::SuspenseWrapper;
use crate::router::Route;
const POSTS_PER_PAGE: i32 = 10;
#[component]
pub fn Home() -> Element {
rsx! { HomeContent { page: 1 } }
rsx! { HomePage { page: 1 } }
}
#[component]
pub fn HomePage(page: i32) -> Element {
rsx! { HomeContent { page } }
}
#[component]
fn HomeContent(page: i32) -> Element {
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 show_skeleton = use_delayed_loading(move || posts_res.read().is_none());
let current_page = page.max(1);
rsx! {
PageLayout { nav_items,
HomeInfo {}
match &*posts_res.read() {
Some(Ok(PostListResponse { 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",
"暂无文章"
}
}
Pagination { current_page, posts: posts.clone() }
SuspenseWrapper {
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! {
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)) => {
rsx! {
div { class: "text-center text-red-500 dark:text-red-400 py-20",
"加载失败: {e}"
}
}
Pagination { current_page, posts: posts.clone() }
}
}
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 {
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" }
}
}
}
}
}
}
_ => {
rsx! {
div { class: "text-center text-gray-500 dark:text-[#9b9c9d] py-20",
"加载中..."
}
}
}

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::page_layout::PageLayout;
use crate::components::post_card::PostCard;
use crate::hooks::delayed_loading::use_delayed_loading;
use crate::components::suspense_wrapper::SuspenseWrapper;
use crate::router::Route;
#[component]
pub fn Tags() -> Element {
let route = use_route::<Route>();
let tags_res = use_resource(list_tags);
let nav_items = use_nav_items(route);
let show_skeleton = use_delayed_loading(move || tags_res.read().is_none());
rsx! {
PageLayout { nav_items,
@ -20,72 +18,63 @@ pub fn Tags() -> Element {
h1 { class: "text-[34px] font-bold text-gray-900 dark:text-[#dadadb]",
"标签"
}
match &*tags_res.read() {
Some(Ok(TagListResponse { tags })) => {
let total = tags.iter().map(|t| t.post_count).sum::<i64>();
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}" }
" 篇文章"
}
}
}
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]",
"加载中..."
}
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>();
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}" }
" 篇文章"
}
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 })) => {
let tags = tags.clone();
rsx! {
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",
"加载失败"
}
Some(Err(_)) => {
rsx! {
div { class: "text-center text-red-500 dark:text-red-400 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" }
}
}
}
}
}
_ => {
rsx! {
div { class: "text-center text-gray-500 dark:text-[#9b9c9d] py-20",
"加载中..."
}
}
}
@ -95,10 +84,7 @@ pub fn Tags() -> Element {
#[component]
pub fn TagDetail(tag: String) -> Element {
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 show_skeleton = use_delayed_loading(move || posts_res.read().is_none());
rsx! {
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]",
"{tag}"
}
match &*posts_res.read() {
Some(Ok(PostListResponse { posts })) => {
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()}" }
" 篇文章"
}
}
}
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]",
"加载中..."
}
}
}
}
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! {
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 })) => {
rsx! {
for post in posts.iter() {
PostCard { post: post.clone() }
}
}
}
Some(Err(_)) => {
rsx! {
div { class: "text-center text-red-500 dark:text-red-400 py-20",
"加载失败"
}
Some(Err(_)) => {
rsx! {
div { class: "text-center text-red-500 dark:text-red-400 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" }
}
}
}
}
}
}
_ => {
rsx! {
div { class: "text-center text-gray-500 dark:text-[#9b9c9d] py-20",
"加载中..."
}
}
}