//! 归档页面模块。 //! //! 对应路由 `/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; use crate::api::posts::{list_published_posts, PostListResponse}; use crate::components::skeletons::archive_skeleton::ArchiveSkeleton; use crate::components::skeletons::delayed_skeleton::DelayedSkeleton; use crate::models::post::PostListItem; use crate::router::Route; /// 按年份分组的文章归档结构。 #[derive(Clone, PartialEq)] struct YearGroup { year: String, months: Vec, } /// 按月份分组的文章归档结构。 #[derive(Clone, PartialEq)] struct MonthGroup { month: String, month_en: String, posts: Vec, } /// 将文章列表按 `formatted_date()` 返回的 `YYYY-MM-DD` 格式进行年、月分组。 /// /// 返回的结果按原始文章顺序组织,调用前已按发布时间降序排列。 fn group_posts(posts: &[PostListItem]) -> 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", "03" => "March", "04" => "April", "05" => "May", "06" => "June", "07" => "July", "08" => "August", "09" => "September", "10" => "October", "11" => "November", "12" => "December", _ => month_num, }; // 尝试追加到当前年份与月份的组中;如果不匹配则新建分组。 if let Some(yg) = years.last_mut() { if yg.year == year { if let Some(mg) = yg.months.last_mut() { if mg.month_en == month_en { mg.posts.push(post.clone()); continue; } } yg.months.push(MonthGroup { month: month_en.to_string(), month_en: month_en.to_string(), posts: vec![post.clone()], }); continue; } } years.push(YearGroup { year, months: vec![MonthGroup { month: month_en.to_string(), month_en: month_en.to_string(), posts: vec![post.clone()], }], }); } years } /// 归档页面组件,对应路由 `/archives`。 /// /// 渲染页面标题,并委托给 `ArchivesContent` 展示按年月分组的文章列表。 #[component] pub fn Archives() -> Element { rsx! { header { class: "page-header mb-6", h1 { class: "text-4xl font-bold text-paper-primary tracking-tight", "归档" } } ArchivesContent {} } } /// 归档页面内容组件。 /// /// 通过 `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(); match &*posts_data { Some(Ok(PostListResponse { posts, total })) => { let grouped = group_posts(posts); rsx! { div { class: "mt-2 text-base text-paper-secondary", "共 " span { class: "font-medium text-paper-primary", "{total}" } " 篇文章" } for year_group in grouped.iter() { YearSection { year_group: year_group.clone() } } } } Some(Err(e)) => { rsx! { div { class: "text-center text-red-500 dark:text-red-400 py-20", "加载失败: {e}" } } } None => { rsx! { DelayedSkeleton { ArchiveSkeleton {} } } } } } /// 单一年份归档区块组件,展示该年份下的所有月份分组。 #[component] fn YearSection(year_group: YearGroup) -> Element { let total = year_group .months .iter() .map(|m| m.posts.len()) .sum::(); rsx! { div { class: "archive-year mt-10", h2 { class: "archive-year-header text-2xl font-bold text-paper-primary mb-4", id: "{year_group.year}", a { class: "archive-header-link hover:opacity-80 transition-opacity", href: "#{year_group.year}", "{year_group.year}" } sup { class: "archive-count text-sm text-paper-secondary ml-1", "{total}" } } for month_group in year_group.months.iter() { MonthSection { month_group: month_group.clone(), year: year_group.year.clone() } } } } } /// 单一月份归档区块组件,展示该月份下的文章条目。 #[component] fn MonthSection(month_group: MonthGroup, year: String) -> Element { let count = month_group.posts.len(); rsx! { div { class: "archive-month flex flex-col md:flex-row md:items-start py-2.5 border-b border-paper-border/50", h3 { class: "archive-month-header text-lg font-medium text-paper-secondary md:w-[200px] shrink-0 mt-0 mb-0 py-1.5", id: "{year}-{month_group.month_en}", a { class: "archive-header-link hover:opacity-80 transition-opacity", href: "#{year}-{month_group.month_en}", "{month_group.month}" } sup { class: "archive-count text-sm text-paper-secondary ml-1", "{count}" } } div { class: "archive-posts flex-1", for post in month_group.posts.iter() { ArchiveEntry { post: post.clone() } } } } } } /// 单条归档文章组件,展示标题与发布日期,并通过覆盖层链接到文章详情。 #[component] fn ArchiveEntry(post: PostListItem) -> Element { let date_str = post.formatted_date(); rsx! { div { class: "archive-entry relative py-1.5 my-2.5 group", h3 { class: "archive-entry-title text-base font-normal text-paper-primary m-0", "{post.title}" } div { class: "archive-meta text-sm text-paper-secondary mt-1", "{date_str}" } Link { class: "entry-link absolute inset-0 z-10", aria_label: "post link to {post.title}", to: Route::PostDetail { slug: post.slug.clone() }, } } } }