docs(pages-frontend): 补充中文注释
This commit is contained in:
parent
1904907add
commit
abfab19839
@ -1,5 +1,15 @@
|
||||
//! 关于页面模块。
|
||||
//!
|
||||
//! 对应路由 `/about`。
|
||||
//!
|
||||
//! 该页面为静态展示页面,不发起任何 server function 调用,
|
||||
//! 直接渲染博客介绍、技术栈与主要特性。
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
/// 关于页面组件,对应路由 `/about`。
|
||||
///
|
||||
/// 展示 Yggdrasil 博客的简介、技术栈与功能特性。
|
||||
#[component]
|
||||
pub fn About() -> Element {
|
||||
rsx! {
|
||||
|
||||
@ -1,3 +1,12 @@
|
||||
//! 归档页面模块。
|
||||
//!
|
||||
//! 对应路由 `/archives`。
|
||||
//!
|
||||
//! 数据获取:通过 `use_server_future` 调用 `list_published_posts(1, 10000)` server function,
|
||||
//! 一次性拉取全部已发布文章,然后在内存中按发布日期的年、月进行分组展示。
|
||||
//! 在 `wasm32` 目标下,server function 的函数体被替换为向服务端端点发起 HTTP POST 请求的客户端存根;
|
||||
//! 实际的数据库访问逻辑仅在 `feature = "server"` 启用时运行。
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus::router::components::Link;
|
||||
|
||||
@ -7,12 +16,14 @@ use crate::components::skeletons::delayed_skeleton::DelayedSkeleton;
|
||||
use crate::models::post::Post;
|
||||
use crate::router::Route;
|
||||
|
||||
/// 按年份分组的文章归档结构。
|
||||
#[derive(Clone, PartialEq)]
|
||||
struct YearGroup {
|
||||
year: String,
|
||||
months: Vec<MonthGroup>,
|
||||
}
|
||||
|
||||
/// 按月份分组的文章归档结构。
|
||||
#[derive(Clone, PartialEq)]
|
||||
struct MonthGroup {
|
||||
month: String,
|
||||
@ -20,18 +31,23 @@ struct MonthGroup {
|
||||
posts: Vec<Post>,
|
||||
}
|
||||
|
||||
/// 将文章列表按 `formatted_date()` 返回的 `YYYY-MM-DD` 格式进行年、月分组。
|
||||
///
|
||||
/// 返回的结果按原始文章顺序组织,调用前已按发布时间降序排列。
|
||||
fn group_posts(posts: &[Post]) -> Vec<YearGroup> {
|
||||
let mut years: Vec<YearGroup> = vec![];
|
||||
|
||||
for post in posts {
|
||||
let date_str = post.formatted_date();
|
||||
|
||||
// 将日期字符串拆分为 [年, 月, 日] 三部分。
|
||||
let parts: Vec<&str> = date_str.split('-').collect();
|
||||
if parts.len() != 3 {
|
||||
continue;
|
||||
}
|
||||
let year = parts[0].to_string();
|
||||
let month_num = parts[1];
|
||||
// 将数字月份转换为英文月份名称,用于展示与锚点 id。
|
||||
let month_en = match month_num {
|
||||
"01" => "January",
|
||||
"02" => "February",
|
||||
@ -48,6 +64,7 @@ fn group_posts(posts: &[Post]) -> Vec<YearGroup> {
|
||||
_ => month_num,
|
||||
};
|
||||
|
||||
// 尝试追加到当前年份与月份的组中;如果不匹配则新建分组。
|
||||
if let Some(yg) = years.last_mut() {
|
||||
if yg.year == year {
|
||||
if let Some(mg) = yg.months.last_mut() {
|
||||
@ -77,6 +94,9 @@ fn group_posts(posts: &[Post]) -> Vec<YearGroup> {
|
||||
years
|
||||
}
|
||||
|
||||
/// 归档页面组件,对应路由 `/archives`。
|
||||
///
|
||||
/// 渲染页面标题,并委托给 `ArchivesContent` 展示按年月分组的文章列表。
|
||||
#[component]
|
||||
pub fn Archives() -> Element {
|
||||
rsx! {
|
||||
@ -89,8 +109,13 @@ pub fn Archives() -> Element {
|
||||
}
|
||||
}
|
||||
|
||||
/// 归档页面内容组件。
|
||||
///
|
||||
/// 通过 `use_server_future` 获取全部已发布文章,按年月分组后渲染;
|
||||
/// 加载中显示骨架屏,失败显示错误提示。
|
||||
#[component]
|
||||
fn ArchivesContent() -> Element {
|
||||
// 一次性获取足够多的已发布文章,用于生成完整的年/月归档。
|
||||
let posts_res = use_server_future(move || list_published_posts(1, 10000))?;
|
||||
|
||||
let posts_data = posts_res.read();
|
||||
@ -123,6 +148,7 @@ fn ArchivesContent() -> Element {
|
||||
}
|
||||
}
|
||||
|
||||
/// 单一年份归档区块组件,展示该年份下的所有月份分组。
|
||||
#[component]
|
||||
fn YearSection(year_group: YearGroup) -> Element {
|
||||
let total = year_group
|
||||
@ -150,6 +176,7 @@ fn YearSection(year_group: YearGroup) -> Element {
|
||||
}
|
||||
}
|
||||
|
||||
/// 单一月份归档区块组件,展示该月份下的文章条目。
|
||||
#[component]
|
||||
fn MonthSection(month_group: MonthGroup, year: String) -> Element {
|
||||
let count = month_group.posts.len();
|
||||
@ -175,6 +202,7 @@ fn MonthSection(month_group: MonthGroup, year: String) -> Element {
|
||||
}
|
||||
}
|
||||
|
||||
/// 单条归档文章组件,展示标题与发布日期,并通过覆盖层链接到文章详情。
|
||||
#[component]
|
||||
fn ArchiveEntry(post: Post) -> Element {
|
||||
let date_str = post.formatted_date();
|
||||
|
||||
@ -1,3 +1,14 @@
|
||||
//! 首页模块。
|
||||
//!
|
||||
//! 对应路由:
|
||||
//! - `/`:首页,默认展示第 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;
|
||||
|
||||
@ -7,13 +18,20 @@ 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);
|
||||
@ -24,10 +42,16 @@ pub fn HomePage(page: i32) -> Element {
|
||||
}
|
||||
}
|
||||
|
||||
/// 首页文章列表与分页展示组件。
|
||||
///
|
||||
/// 通过 `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()),
|
||||
@ -39,11 +63,13 @@ fn HomePosts(current_page: i32) -> Element {
|
||||
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 }
|
||||
}
|
||||
}
|
||||
@ -62,6 +88,7 @@ fn HomePosts(current_page: i32) -> Element {
|
||||
}
|
||||
}
|
||||
|
||||
/// 首页头部信息组件,展示站点名称与副标题。
|
||||
#[component]
|
||||
fn HomeInfo() -> Element {
|
||||
rsx! {
|
||||
@ -76,12 +103,18 @@ fn HomeInfo() -> Element {
|
||||
}
|
||||
}
|
||||
|
||||
/// 分页导航组件。
|
||||
///
|
||||
/// 根据当前页码与文章总数计算总页数,并渲染上一页/下一页链接。
|
||||
/// 第一页的上一页链接固定指向 `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 {
|
||||
|
||||
@ -1,8 +1,18 @@
|
||||
//! 404 页面模块。
|
||||
//!
|
||||
//! 对应路由 `/:..segments`。
|
||||
//!
|
||||
//! 当用户访问未匹配任何前端路由的 URL 时,Dioxus Router 会回退到该 404 页面。
|
||||
//! 该页面为静态展示页面,不发起任何 server function 调用。
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus::router::components::Link;
|
||||
|
||||
use crate::router::Route;
|
||||
|
||||
/// 404 页面组件,对应兜底路由 `/:..segments`。
|
||||
///
|
||||
/// 展示大号的装饰性 404 数字、状态标签、错误说明以及返回首页的链接。
|
||||
#[component]
|
||||
pub fn NotFound(segments: Vec<String>) -> Element {
|
||||
let _ = segments;
|
||||
|
||||
@ -1,3 +1,14 @@
|
||||
//! 文章详情页面模块。
|
||||
//!
|
||||
//! 对应路由 `/post/:slug`。
|
||||
//!
|
||||
//! 数据获取:通过 `use_server_future` 调用 `get_post_by_slug` server function,
|
||||
//! 根据 URL 中的 slug 获取单篇文章详情(含正文 HTML、目录、封面及上下篇导航)。
|
||||
//! 由于 Dioxus 组件参数在路由切换时可能复用同一组件实例,
|
||||
//! 这里使用 `use_signal` 保存当前 slug,并在参数变化时更新信号以触发重新取数。
|
||||
//! 在 `wasm32` 目标下,server function 的函数体被替换为向服务端端点发起 HTTP POST 请求的客户端存根;
|
||||
//! 实际的数据库访问逻辑仅在 `feature = "server"` 启用时运行。
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus::router::components::Link;
|
||||
|
||||
@ -11,18 +22,25 @@ use crate::components::skeletons::delayed_skeleton::DelayedSkeleton;
|
||||
use crate::components::skeletons::post_detail_skeleton::PostDetailSkeleton;
|
||||
use crate::router::Route;
|
||||
|
||||
/// 文章详情页面组件,对应路由 `/post/:slug`。
|
||||
///
|
||||
/// 根据 slug 异步获取文章,渲染文章头部、封面、目录、正文、页脚及评论区;
|
||||
/// 若文章不存在或加载失败,则展示对应的提示页面。
|
||||
#[component]
|
||||
pub fn PostDetail(slug: String) -> Element {
|
||||
// 使用信号保存当前 slug,以便在路由参数变化时重新触发 server future。
|
||||
let mut slug_signal = use_signal(|| slug.clone());
|
||||
if slug_signal() != slug {
|
||||
slug_signal.set(slug.clone());
|
||||
}
|
||||
|
||||
// 当 slug 信号变化时,自动重新调用 server function 获取文章详情。
|
||||
let post = use_server_future(move || {
|
||||
let s = slug_signal();
|
||||
get_post_by_slug(s)
|
||||
})?;
|
||||
|
||||
// 将结果映射为更直观的 Ok(post) / Err("not_found") / Err("error") 三种状态。
|
||||
let post_data = post.read().as_ref().map(|r| match r {
|
||||
Ok(SinglePostResponse { post: Some(post) }) => Ok(post.clone()),
|
||||
Ok(SinglePostResponse { post: None }) => Err("not_found"),
|
||||
@ -35,10 +53,12 @@ pub fn PostDetail(slug: String) -> Element {
|
||||
article { class: "post-single",
|
||||
PostHeader { post: post.clone() }
|
||||
|
||||
// 如果文章设置了封面图,则渲染封面组件。
|
||||
if let Some(cover) = &post.cover_image {
|
||||
PostCover { src: cover.clone() }
|
||||
}
|
||||
|
||||
// 如果文章生成了目录 HTML,则渲染目录组件。
|
||||
if let Some(toc) = &post.toc_html {
|
||||
PostToc { toc_html: toc.clone() }
|
||||
}
|
||||
@ -49,6 +69,7 @@ pub fn PostDetail(slug: String) -> Element {
|
||||
|
||||
PostFooter { post: post.clone() }
|
||||
|
||||
// 仅对已发布文章展示评论区域,使用 SuspenseBoundary 处理加载状态。
|
||||
if post.status == crate::models::post::PostStatus::Published {
|
||||
div { class: "mt-12 border-t border-gray-200 dark:border-[#333] pt-8",
|
||||
SuspenseBoundary {
|
||||
|
||||
@ -1,3 +1,13 @@
|
||||
//! 搜索页面模块。
|
||||
//!
|
||||
//! 对应路由 `/search`。
|
||||
//!
|
||||
//! 数据获取:用户在输入框中键入关键词并触发搜索后,
|
||||
//! 通过 Dioxus 的 `spawn` 在本地启动异步任务,调用 `search_posts` server function。
|
||||
//! 与首页/归档不同,搜索是交互式客户端行为,不在服务端渲染阶段预取数据。
|
||||
//! 在 `wasm32` 目标下,该 server function 的函数体被替换为向服务端端点发起 HTTP POST 请求的客户端存根;
|
||||
//! 实际的数据库访问逻辑仅在 `feature = "server"` 启用时运行。
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
use crate::api::posts::{search_posts, PostListResponse};
|
||||
@ -5,11 +15,19 @@ use crate::components::post_card::PostCard;
|
||||
use crate::components::skeletons::delayed_skeleton::DelayedSkeleton;
|
||||
use crate::components::skeletons::search_skeleton::SearchSkeleton;
|
||||
|
||||
/// 搜索页面组件,对应路由 `/search`。
|
||||
///
|
||||
/// 维护搜索关键词、搜索结果与加载状态,渲染搜索框与结果列表。
|
||||
/// 结果通过客户端异步请求获取,而非 `use_server_future` 预取。
|
||||
#[component]
|
||||
pub fn Search() -> Element {
|
||||
// 当前输入框中的搜索关键词。
|
||||
let mut query = use_signal(|| "".to_string());
|
||||
// 搜索结果:None 表示尚未执行搜索或已清空。
|
||||
let mut search_res = use_signal(|| None::<Result<PostListResponse, ServerFnError>>);
|
||||
// 是否正在发起搜索请求。
|
||||
let mut is_searching = use_signal(|| false);
|
||||
// 触发搜索的回调:校验空查询后启动异步请求。
|
||||
let mut on_search = move || {
|
||||
let q = query().trim().to_string();
|
||||
if q.is_empty() {
|
||||
@ -47,6 +65,7 @@ pub fn Search() -> Element {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 根据搜索状态展示骨架屏、结果列表、空状态或错误提示。
|
||||
if is_searching() {
|
||||
DelayedSkeleton { SearchSkeleton {} }
|
||||
} else if let Some(Ok(PostListResponse { posts, total: _ })) = search_res() {
|
||||
|
||||
@ -1,3 +1,15 @@
|
||||
//! 标签页面模块。
|
||||
//!
|
||||
//! 对应路由:
|
||||
//! - `/tags`:标签云,展示所有标签及关联文章数量。
|
||||
//! - `/tags/:tag`:标签详情页,展示指定标签下的已发布文章列表。
|
||||
//!
|
||||
//! 数据获取:
|
||||
//! - 标签云通过 `use_server_future(list_tags)` 获取全部标签信息。
|
||||
//! - 标签详情通过 `use_server_future` 调用 `get_posts_by_tag(tag)` 获取该标签下的文章列表。
|
||||
//! 在 `wasm32` 目标下,这些 server function 的函数体被替换为向服务端端点发起 HTTP POST 请求的客户端存根;
|
||||
//! 实际的数据库访问逻辑仅在 `feature = "server"` 启用时运行。
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus::router::components::Link;
|
||||
|
||||
@ -7,6 +19,9 @@ use crate::components::skeletons::delayed_skeleton::DelayedSkeleton;
|
||||
use crate::components::skeletons::tags_skeleton::{TagDetailSkeleton, TagsSkeleton};
|
||||
use crate::router::Route;
|
||||
|
||||
/// 标签云页面组件,对应路由 `/tags`。
|
||||
///
|
||||
/// 渲染页面标题,并委托给 `TagsContent` 展示所有标签。
|
||||
#[component]
|
||||
pub fn Tags() -> Element {
|
||||
rsx! {
|
||||
@ -19,10 +34,15 @@ pub fn Tags() -> Element {
|
||||
}
|
||||
}
|
||||
|
||||
/// 标签云内容组件。
|
||||
///
|
||||
/// 通过 `use_server_future(list_tags)` 异步获取标签列表;
|
||||
/// 成功时渲染标签总数、文章总数以及每个标签的链接。
|
||||
#[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()),
|
||||
@ -68,6 +88,9 @@ fn TagsContent() -> Element {
|
||||
}
|
||||
}
|
||||
|
||||
/// 标签详情页面组件,对应路由 `/tags/:tag`。
|
||||
///
|
||||
/// 渲染当前标签名称,并委托给 `TagDetailContent` 展示该标签下的文章列表。
|
||||
#[component]
|
||||
pub fn TagDetail(tag: String) -> Element {
|
||||
rsx! {
|
||||
@ -80,10 +103,15 @@ pub fn TagDetail(tag: String) -> Element {
|
||||
}
|
||||
}
|
||||
|
||||
/// 标签详情内容组件。
|
||||
///
|
||||
/// 通过 `use_server_future` 调用 `get_posts_by_tag` 获取指定标签下的文章;
|
||||
/// 成功时渲染文章总数与文章卡片。
|
||||
#[component]
|
||||
fn TagDetailContent(tag: String) -> Element {
|
||||
let posts_res = use_server_future(move || get_posts_by_tag(tag.clone()))?;
|
||||
|
||||
// 将结果映射为 (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()),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user