145 lines
5.1 KiB
Rust
145 lines
5.1 KiB
Rust
//! 首页模块。
|
||
//!
|
||
//! 对应路由:
|
||
//! - `/`:首页,默认展示第 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", "»" }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|