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