yggdrasil/pages/
archives.rs1use dioxus::prelude::*;
11use dioxus::router::components::Link;
12
13use crate::api::posts::{list_published_posts, PostListResponse};
14use crate::components::empty_state::EmptyState;
15use crate::components::skeletons::archive_skeleton::ArchiveSkeleton;
16use crate::components::skeletons::delayed_skeleton::DelayedSkeleton;
17use crate::models::post::PostListItem;
18use crate::router::Route;
19
20#[derive(Clone, PartialEq)]
22struct YearGroup {
23 year: String,
24 months: Vec<MonthGroup>,
25}
26
27#[derive(Clone, PartialEq)]
29struct MonthGroup {
30 month: String,
31 month_en: String,
32 posts: Vec<PostListItem>,
33}
34
35fn group_posts(posts: &[PostListItem]) -> Vec<YearGroup> {
39 let mut years: Vec<YearGroup> = vec![];
40
41 for post in posts {
42 let date_str = post.formatted_date();
43
44 let parts: Vec<&str> = date_str.split('-').collect();
46 if parts.len() != 3 {
47 continue;
48 }
49 let year = parts[0].to_string();
50 let month_num = parts[1];
51 let month_en = match month_num {
53 "01" => "January",
54 "02" => "February",
55 "03" => "March",
56 "04" => "April",
57 "05" => "May",
58 "06" => "June",
59 "07" => "July",
60 "08" => "August",
61 "09" => "September",
62 "10" => "October",
63 "11" => "November",
64 "12" => "December",
65 _ => month_num,
66 };
67
68 if let Some(yg) = years.last_mut() {
70 if yg.year == year {
71 if let Some(mg) = yg.months.last_mut() {
72 if mg.month_en == month_en {
73 mg.posts.push(post.clone());
74 continue;
75 }
76 }
77 yg.months.push(MonthGroup {
78 month: month_en.to_string(),
79 month_en: month_en.to_string(),
80 posts: vec![post.clone()],
81 });
82 continue;
83 }
84 }
85 years.push(YearGroup {
86 year,
87 months: vec![MonthGroup {
88 month: month_en.to_string(),
89 month_en: month_en.to_string(),
90 posts: vec![post.clone()],
91 }],
92 });
93 }
94
95 years
96}
97
98#[component]
102pub fn Archives() -> Element {
103 rsx! {
104 div { class: "animate-page-enter",
105 header { class: "page-header mb-6",
106 h1 { class: "text-4xl font-bold text-paper-primary tracking-tight",
107 "归档"
108 }
109 }
110 ArchivesContent {}
111 }
112 }
113}
114
115#[component]
120fn ArchivesContent() -> Element {
121 let posts_res = use_server_future(move || list_published_posts(1, 10000))?;
123
124 let posts_data = posts_res.read();
125 match &*posts_data {
126 Some(Ok(PostListResponse { posts, total })) => {
127 if *total == 0 {
128 rsx! {
129 EmptyState {
130 title: "还没有文章归档",
131 description: "发布文章后,这里会自动按年月进行归档显示。",
132 }
133 }
134 } else {
135 let grouped = group_posts(posts);
136 rsx! {
137 div { class: "mt-2 text-base text-paper-secondary",
138 "共 "
139 span { class: "font-medium text-paper-primary", "{total}" }
140 " 篇文章"
141 }
142 for year_group in grouped.iter() {
143 YearSection {
144 key: "{year_group.year}",
145 year_group: year_group.clone(),
146 }
147 }
148 }
149 }
150 }
151 Some(Err(e)) => {
152 rsx! {
153 div { class: "text-center text-red-500 dark:text-red-400 py-20", "加载失败: {e}" }
154 }
155 }
156 None => {
157 rsx! {
158 DelayedSkeleton { ArchiveSkeleton {} }
159 }
160 }
161 }
162}
163
164#[component]
166fn YearSection(year_group: YearGroup) -> Element {
167 let total = year_group
168 .months
169 .iter()
170 .map(|m| m.posts.len())
171 .sum::<usize>();
172
173 rsx! {
174 div { class: "archive-year mt-10",
175 h2 {
176 class: "archive-year-header text-2xl font-bold text-paper-primary mb-4",
177 id: "{year_group.year}",
178 a {
179 class: "archive-header-link hover:opacity-80 transition-opacity",
180 href: "#{year_group.year}",
181 "{year_group.year}"
182 }
183 sup { class: "archive-count text-sm text-paper-secondary ml-1", "{total}" }
184 }
185 for month_group in year_group.months.iter() {
186 MonthSection {
187 key: "{month_group.month_en}",
188 month_group: month_group.clone(),
189 year: year_group.year.clone(),
190 }
191 }
192 }
193 }
194}
195
196#[component]
198fn MonthSection(month_group: MonthGroup, year: String) -> Element {
199 let count = month_group.posts.len();
200
201 rsx! {
202 div { class: "archive-month flex flex-col md:flex-row md:items-start py-2.5 border-b border-paper-border/50",
203 h3 {
204 class: "archive-month-header text-lg font-medium text-paper-secondary md:w-[200px] shrink-0 mt-0 mb-0 py-1.5",
205 id: "{year}-{month_group.month_en}",
206 a {
207 class: "archive-header-link hover:opacity-80 transition-opacity",
208 href: "#{year}-{month_group.month_en}",
209 "{month_group.month}"
210 }
211 sup { class: "archive-count text-sm text-paper-secondary ml-1", "{count}" }
212 }
213 div { class: "archive-posts flex-1",
214 for post in month_group.posts.iter() {
215 ArchiveEntry { key: "{post.id}", post: post.clone() }
216 }
217 }
218 }
219 }
220}
221
222#[component]
224fn ArchiveEntry(post: PostListItem) -> Element {
225 let date_str = post.formatted_date();
226
227 rsx! {
228 div { class: "archive-entry relative py-1.5 my-2.5 group",
229 h3 { class: "archive-entry-title text-base font-normal text-paper-primary m-0",
230 "{post.title}"
231 }
232 div { class: "archive-meta text-sm text-paper-secondary mt-1", "{date_str}" }
233 Link {
234 class: "entry-link absolute inset-0 z-10",
235 aria_label: "post link to {post.title}",
236 to: Route::PostDetail {
237 slug: post.slug.clone(),
238 },
239 }
240 }
241 }
242}