yggdrasil/src/pages/home.rs

145 lines
5.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 首页模块。
//!
//! 对应路由:
//! - `/`:首页,默认展示第 1 页文章。
//! - `/page/:page`:分页首页,展示指定页码的已发布文章列表。
//!
//! 数据获取:通过 `use_server_future` 调用 `list_published_posts` server function
//! 从服务端获取已发布文章的分页列表与总数,并渲染文章卡片与分页导航。
//! 在 `wasm32` 目标下server function 的函数体被替换为向服务端端点发起 HTTP POST 请求的客户端存根;
//! 实际的数据库访问逻辑仅在 `feature = "server"` 启用时运行。
use dioxus::prelude::*;
use dioxus::router::components::Link;
use crate::api::posts::{list_published_posts, PostListResponse};
use crate::components::post_card::PostCard;
use crate::components::skeletons::delayed_skeleton::DelayedSkeleton;
use crate::components::skeletons::home_skeleton::HomeSkeleton;
use crate::router::Route;
// 每页展示的已发布文章数量,用于分页计算。
const POSTS_PER_PAGE: i32 = 10;
/// 首页组件,对应路由 `/`。
///
/// 直接委托给 `HomePage` 并固定页码为 1。
#[component]
pub fn Home() -> Element {
rsx! { HomePage { page: 1 } }
}
/// 首页分页组件,对应路由 `/page/:page`。
///
/// 对传入的页码进行下限校正后,渲染头部信息与文章列表。
#[component]
pub fn HomePage(page: i32) -> Element {
let current_page = page.max(1);
rsx! {
HomeInfo {}
HomePosts { current_page }
}
}
/// 首页文章列表与分页展示组件。
///
/// 通过 `use_server_future` 异步获取当前页文章;
/// 加载中显示骨架屏,加载失败显示错误提示,成功则渲染文章卡片与分页。
#[component]
fn HomePosts(current_page: i32) -> Element {
// 调用 server function 获取已发布文章分页数据。
let posts_res = use_server_future(move || list_published_posts(current_page, POSTS_PER_PAGE))?;
// 将结果映射为更便于本地使用的 (posts, total) 形式。
let posts_data = posts_res.read().as_ref().map(|r| match r {
Ok(PostListResponse { posts, total }) => Ok((posts.clone(), *total)),
Err(e) => Err(e.to_string()),
});
match posts_data {
Some(Ok((posts, total))) => {
rsx! {
for post in posts.iter() {
PostCard { post: post.clone() }
}
// 如果当前页没有任何文章,显示空状态提示。
if posts.is_empty() {
div { class: "text-center text-paper-secondary py-20",
"暂无文章"
}
}
// 在列表底部渲染分页导航。
Pagination { current_page, total }
}
}
Some(Err(e)) => {
rsx! {
div { class: "text-center text-red-500 dark:text-red-400 py-20",
"加载失败: {e}"
}
}
}
_ => {
rsx! {
DelayedSkeleton { HomeSkeleton {} }
}
}
}
}
/// 首页头部信息组件,展示站点名称与副标题。
#[component]
fn HomeInfo() -> Element {
rsx! {
div { class: "mb-10 text-center",
h1 { class: "text-4xl font-bold leading-tight text-paper-primary tracking-tight",
"Yggdrasil"
}
p { class: "mt-3 text-base text-paper-secondary leading-relaxed",
"以文字为主的简约博客系统"
}
}
}
}
/// 分页导航组件。
///
/// 根据当前页码与文章总数计算总页数,并渲染上一页/下一页链接。
/// 第一页的上一页链接固定指向 `Route::Home`,避免生成 `/page/1`。
#[component]
fn Pagination(current_page: i32, total: i64) -> Element {
let has_prev = current_page > 1;
// 向上取整计算总页数,至少为 1 页。
let total_pages = ((total + POSTS_PER_PAGE as i64 - 1) / POSTS_PER_PAGE as i64).max(1) as i32;
let has_next = current_page < total_pages;
let prev = current_page - 1;
// 当上一页为第 1 页时,使用 `/` 路由而非 `/page/1`。
let prev_route = if prev <= 1 {
Route::Home {}
} else {
Route::HomePage { page: prev }
};
rsx! {
nav { class: "flex mt-10 mb-6 justify-between",
if has_prev {
Link {
class: "inline-flex items-center px-4 py-2 text-sm text-white bg-paper-accent rounded-full hover:brightness-110 active:scale-[0.98] transition-all duration-200 cursor-pointer",
to: prev_route,
span { class: "mr-1", "«" }
"上一页"
}
}
if has_next {
Link {
class: "ml-auto inline-flex items-center px-4 py-2 text-sm text-white bg-paper-accent rounded-full hover:brightness-110 active:scale-[0.98] transition-all duration-200 cursor-pointer",
to: Route::HomePage { page: current_page + 1 },
"下一页"
span { class: "ml-1", "»" }
}
}
}
}
}