diff --git a/src/pages/about.rs b/src/pages/about.rs index d7ea6b4..e5c6e5c 100644 --- a/src/pages/about.rs +++ b/src/pages/about.rs @@ -1,5 +1,15 @@ +//! 关于页面模块。 +//! +//! 对应路由 `/about`。 +//! +//! 该页面为静态展示页面,不发起任何 server function 调用, +//! 直接渲染博客介绍、技术栈与主要特性。 + use dioxus::prelude::*; +/// 关于页面组件,对应路由 `/about`。 +/// +/// 展示 Yggdrasil 博客的简介、技术栈与功能特性。 #[component] pub fn About() -> Element { rsx! { diff --git a/src/pages/archives.rs b/src/pages/archives.rs index c7f4b53..126176f 100644 --- a/src/pages/archives.rs +++ b/src/pages/archives.rs @@ -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, } +/// 按月份分组的文章归档结构。 #[derive(Clone, PartialEq)] struct MonthGroup { month: String, @@ -20,18 +31,23 @@ struct MonthGroup { posts: Vec, } +/// 将文章列表按 `formatted_date()` 返回的 `YYYY-MM-DD` 格式进行年、月分组。 +/// +/// 返回的结果按原始文章顺序组织,调用前已按发布时间降序排列。 fn group_posts(posts: &[Post]) -> Vec { let mut years: Vec = 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 { _ => 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 { 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(); diff --git a/src/pages/home.rs b/src/pages/home.rs index 18498ce..0a30b1f 100644 --- a/src/pages/home.rs +++ b/src/pages/home.rs @@ -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 { diff --git a/src/pages/not_found.rs b/src/pages/not_found.rs index a4d7139..5a58d88 100644 --- a/src/pages/not_found.rs +++ b/src/pages/not_found.rs @@ -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) -> Element { let _ = segments; diff --git a/src/pages/post_detail.rs b/src/pages/post_detail.rs index 3a9587c..41927a1 100644 --- a/src/pages/post_detail.rs +++ b/src/pages/post_detail.rs @@ -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 { diff --git a/src/pages/search.rs b/src/pages/search.rs index aff4231..9294bf2 100644 --- a/src/pages/search.rs +++ b/src/pages/search.rs @@ -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::>); + // 是否正在发起搜索请求。 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() { diff --git a/src/pages/tags.rs b/src/pages/tags.rs index d581fce..6f6a388 100644 --- a/src/pages/tags.rs +++ b/src/pages/tags.rs @@ -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()),