From 7599bfbb13f960acb536c24c5276c5636e31c15c Mon Sep 17 00:00:00 2001 From: xfy Date: Tue, 26 May 2026 13:31:35 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=20PaperMod=20=E9=A3=8E?= =?UTF-8?q?=E6=A0=BC=E5=BD=92=E6=A1=A3=E9=A1=B5=E9=9D=A2=EF=BC=8C=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E6=8C=89=E5=B9=B4=E4=BB=BD=E6=9C=88=E4=BB=BD=E5=88=86?= =?UTF-8?q?=E7=BB=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 ArchivesPage 组件,三级结构:年份 → 月份 → 文章 - Header/NavItem/Footer 改为 pub 共享,NavItem 通过路由动态判断高亮 - 编译 Tailwind CSS 包含归档页面所需 utility classes Co-Authored-By: Claude Opus 4.7 (1M context) --- public/style.css | 68 ++++++++++++++ src/pages/archives.rs | 205 ++++++++++++++++++++++++++++++++++++++++++ src/pages/home.rs | 30 +++++-- src/pages/mod.rs | 1 + src/router.rs | 6 +- 5 files changed, 296 insertions(+), 14 deletions(-) create mode 100644 src/pages/archives.rs diff --git a/public/style.css b/public/style.css index 98548d4..29c97cc 100644 --- a/public/style.css +++ b/public/style.css @@ -21,6 +21,7 @@ --color-blue-600: oklch(54.6% 0.245 262.881); --color-blue-700: oklch(48.8% 0.243 264.376); --color-gray-50: oklch(98.5% 0.002 247.839); + --color-gray-100: oklch(96.7% 0.003 264.542); --color-gray-200: oklch(92.8% 0.006 264.531); --color-gray-300: oklch(87.2% 0.01 258.338); --color-gray-400: oklch(70.7% 0.022 261.325); @@ -38,10 +39,13 @@ --text-sm--line-height: calc(1.25 / 0.875); --text-base: 1rem; --text-base--line-height: calc(1.5 / 1); + --text-lg: 1.125rem; + --text-lg--line-height: calc(1.75 / 1.125); --text-xl: 1.25rem; --text-xl--line-height: calc(1.75 / 1.25); --text-2xl: 1.5rem; --text-2xl--line-height: calc(2 / 1.5); + --font-weight-normal: 400; --font-weight-medium: 500; --font-weight-bold: 700; --leading-tight: 1.25; @@ -210,6 +214,9 @@ .visible { visibility: visible; } + .absolute { + position: absolute; + } .relative { position: relative; } @@ -219,15 +226,33 @@ .sticky { position: sticky; } + .inset-0 { + inset: calc(var(--spacing) * 0); + } .top-0 { top: calc(var(--spacing) * 0); } + .z-10 { + z-index: 10; + } .z-40 { z-index: 40; } + .m-0 { + margin: calc(var(--spacing) * 0); + } .mx-auto { margin-inline: auto; } + .my-2\.5 { + margin-block: calc(var(--spacing) * 2.5); + } + .mt-0 { + margin-top: calc(var(--spacing) * 0); + } + .mt-1 { + margin-top: calc(var(--spacing) * 1); + } .mt-2 { margin-top: calc(var(--spacing) * 2); } @@ -243,6 +268,9 @@ .mt-auto { margin-top: auto; } + .mb-0 { + margin-bottom: calc(var(--spacing) * 0); + } .mb-1 { margin-bottom: calc(var(--spacing) * 1); } @@ -306,6 +334,9 @@ .flex-1 { flex: 1; } + .shrink-0 { + flex-shrink: 0; + } .translate-y-0 { --tw-translate-y: calc(var(--spacing) * 0); translate: var(--tw-translate-x) var(--tw-translate-y); @@ -369,6 +400,9 @@ border-bottom-style: var(--tw-border-style); border-bottom-width: 1px; } + .border-gray-100 { + border-color: var(--color-gray-100); + } .border-gray-200 { border-color: var(--color-gray-200); } @@ -426,9 +460,15 @@ .py-1 { padding-block: calc(var(--spacing) * 1); } + .py-1\.5 { + padding-block: calc(var(--spacing) * 1.5); + } .py-2 { padding-block: calc(var(--spacing) * 2); } + .py-2\.5 { + padding-block: calc(var(--spacing) * 2.5); + } .py-4 { padding-block: calc(var(--spacing) * 4); } @@ -452,6 +492,10 @@ font-size: var(--text-base); line-height: var(--tw-leading, var(--text-base--line-height)); } + .text-lg { + font-size: var(--text-lg); + line-height: var(--tw-leading, var(--text-lg--line-height)); + } .text-sm { font-size: var(--text-sm); line-height: var(--tw-leading, var(--text-sm--line-height)); @@ -482,6 +526,10 @@ --tw-font-weight: var(--font-weight-medium); font-weight: var(--font-weight-medium); } + .font-normal { + --tw-font-weight: var(--font-weight-normal); + font-weight: var(--font-weight-normal); + } .text-blue-600 { color: var(--color-blue-600); } @@ -649,11 +697,31 @@ display: flex; } } + .md\:w-\[200px\] { + @media (width >= 48rem) { + width: 200px; + } + } + .md\:flex-row { + @media (width >= 48rem) { + flex-direction: row; + } + } + .md\:items-start { + @media (width >= 48rem) { + align-items: flex-start; + } + } .dark\:border-\[\#333\] { &:where(.dark, .dark *) { border-color: #333; } } + .dark\:border-\[\#333\]\/50 { + &:where(.dark, .dark *) { + border-color: color-mix(in oklab, #333 50%, transparent); + } + } .dark\:border-gray-600 { &:where(.dark, .dark *) { border-color: var(--color-gray-600); diff --git a/src/pages/archives.rs b/src/pages/archives.rs new file mode 100644 index 0000000..572433c --- /dev/null +++ b/src/pages/archives.rs @@ -0,0 +1,205 @@ +use dioxus::prelude::*; + +use crate::pages::home::{Footer, Header}; + +#[derive(Clone, PartialEq)] +pub struct Post { + pub title: &'static str, + pub date: &'static str, + pub slug: &'static str, +} + +const POSTS: &[Post] = &[ + Post { + title: "开始使用 Rust 构建 Web 应用", + date: "2026-05-20", + slug: "rust-web-app", + }, + Post { + title: "Tailwind CSS 的设计理念与实践", + date: "2026-05-15", + slug: "tailwind-css", + }, + Post { + title: "PostgreSQL 在 Rust 项目中的最佳实践", + date: "2026-05-10", + slug: "postgresql-rust", + }, + Post { + title: "暗色模式的设计思考", + date: "2026-05-05", + slug: "dark-mode-design", + }, + Post { + title: "博客系统的架构演进", + date: "2026-04-28", + slug: "blog-architecture", + }, + Post { + title: "Dioxus 0.7 新特性一览", + date: "2026-04-20", + slug: "dioxus-07", + }, +]; + +#[derive(Clone, PartialEq)] +struct YearGroup { + year: &'static str, + months: Vec, +} + +#[derive(Clone, PartialEq)] +struct MonthGroup { + month: &'static str, + month_en: &'static str, + posts: Vec, +} + +fn group_posts(posts: &[Post]) -> Vec { + let mut years: Vec = vec![]; + + for post in posts { + let parts: Vec<&str> = post.date.split('-').collect(); + if parts.len() != 3 { + continue; + } + let year = parts[0]; + let month_num = parts[1]; + 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, + month_en, + posts: vec![post.clone()], + }); + continue; + } + } + years.push(YearGroup { + year, + months: vec![MonthGroup { + month: month_en, + month_en, + posts: vec![post.clone()], + }], + }); + } + + years +} + +#[component] +pub fn ArchivesPage() -> Element { + let grouped = group_posts(POSTS); + + rsx! { + div { class: "min-h-screen flex flex-col bg-white dark:bg-[#1d1e20] transition-colors duration-300", + Header {} + main { class: "flex-1 w-full max-w-3xl mx-auto px-6 py-6", + header { class: "page-header mb-6", + h1 { class: "text-[34px] font-bold text-gray-900 dark:text-[#dadadb]", + "归档" + } + div { class: "mt-2 text-base text-gray-500 dark:text-[#9b9c9d]", + "共 " + span { class: "font-medium text-gray-700 dark:text-[#dadadb]", "{POSTS.len()}" } + " 篇文章" + } + } + for year_group in grouped.iter() { + YearSection { year_group: year_group.clone() } + } + } + Footer {} + } + } +} + +#[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-gray-900 dark:text-[#dadadb] 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-gray-400 dark:text-[#9b9c9d] ml-1", "{total}" } + } + for month_group in year_group.months.iter() { + MonthSection { month_group: month_group.clone(), year: year_group.year } + } + } + } +} + +#[component] +fn MonthSection(month_group: MonthGroup, year: &'static str) -> 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-gray-100 dark:border-[#333]/50", + h3 { + class: "archive-month-header text-lg font-medium text-gray-700 dark:text-[#9b9c9d] 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-gray-400 dark:text-[#9b9c9d] ml-1", "{count}" } + } + div { class: "archive-posts flex-1", + for post in month_group.posts.iter() { + ArchiveEntry { post: post.clone() } + } + } + } + } +} + +#[component] +fn ArchiveEntry(post: Post) -> Element { + rsx! { + div { class: "archive-entry relative py-1.5 my-2.5 group", + h3 { class: "archive-entry-title text-base font-normal text-gray-900 dark:text-[#dadadb] m-0", + "{post.title}" + } + div { class: "archive-meta text-sm text-gray-400 dark:text-[#9b9c9d] mt-1", + "{post.date}" + } + a { + class: "entry-link absolute inset-0 z-10", + aria_label: "post link to {post.title}", + href: "/post/{post.slug}", + } + } + } +} diff --git a/src/pages/home.rs b/src/pages/home.rs index fbeb197..e47b868 100644 --- a/src/pages/home.rs +++ b/src/pages/home.rs @@ -1,5 +1,6 @@ use dioxus::prelude::*; +use crate::router::Route; use crate::theme::ThemeToggle; #[derive(Clone, PartialEq)] @@ -74,7 +75,9 @@ pub fn HomePage() -> Element { } #[component] -fn Header() -> Element { +pub fn Header() -> Element { + let route = use_route::(); + rsx! { header { class: "sticky top-0 z-40 w-full border-b border-gray-200 dark:border-[#333] bg-white/80 dark:bg-[#1d1e20]/80 backdrop-blur-sm", nav { class: "max-w-3xl mx-auto px-6 h-[60px] flex items-center justify-between", @@ -85,11 +88,11 @@ fn Header() -> Element { } div { class: "flex items-center gap-2", ul { class: "hidden md:flex items-center gap-1", - NavItem { href: "/", label: "首页", active: true } - NavItem { href: "/archives", label: "归档" } - NavItem { href: "/tags", label: "标签" } - NavItem { href: "/search", label: "搜索" } - NavItem { href: "/about", label: "关于" } + NavItem { href: "/", label: "首页", route: route.clone() } + NavItem { href: "/archives", label: "归档", route: route.clone() } + NavItem { href: "/tags", label: "标签", route: route.clone() } + NavItem { href: "/search", label: "搜索", route: route.clone() } + NavItem { href: "/about", label: "关于", route: route.clone() } } ThemeToggle {} } @@ -99,9 +102,18 @@ fn Header() -> Element { } #[component] -fn NavItem(href: &'static str, label: &'static str, #[props(default = false)] active: bool) -> Element { +pub fn NavItem(href: &'static str, label: &'static str, route: Route) -> Element { + let is_active = match (href, route) { + ("/", Route::HomePage {}) => true, + ("/archives", Route::ArchivesPage {}) => true, + ("/tags", Route::TagsPage {}) => true, + ("/search", Route::SearchPage {}) => true, + ("/about", Route::AboutPage {}) => true, + _ => false, + }; + let base_class = "px-3 py-1 text-base rounded-lg transition-colors"; - let class_str = if active { + let class_str = if is_active { format!("{} font-medium text-gray-900 dark:text-[#dadadb] underline underline-offset-[0.3rem] decoration-2 decoration-gray-900 dark:decoration-[#dadadb]", base_class) } else { format!("{} text-gray-600 dark:text-[#9b9c9d] hover:text-gray-900 dark:hover:text-[#dadadb]", base_class) @@ -171,7 +183,7 @@ fn Pagination() -> Element { } #[component] -fn Footer() -> Element { +pub fn Footer() -> Element { let mut visible = use_signal(|| false); use_effect(move || { diff --git a/src/pages/mod.rs b/src/pages/mod.rs index f23c078..27d531e 100644 --- a/src/pages/mod.rs +++ b/src/pages/mod.rs @@ -1,4 +1,5 @@ pub mod admin; +pub mod archives; pub mod home; pub mod login; pub mod register; diff --git a/src/router.rs b/src/router.rs index 76f5999..0388d05 100644 --- a/src/router.rs +++ b/src/router.rs @@ -1,6 +1,7 @@ use dioxus::prelude::*; use crate::pages::admin::AdminPage; +use crate::pages::archives::ArchivesPage; use crate::pages::home::HomePage; use crate::pages::login::LoginPage; use crate::pages::register::RegisterPage; @@ -43,11 +44,6 @@ pub fn AppRouter() -> Element { } } -#[component] -pub fn ArchivesPage() -> Element { - rsx! { "Archives" } -} - #[component] pub fn TagsPage() -> Element { rsx! { "Tags" }