实现 PaperMod 风格归档页面,支持按年份月份分组
- 新增 ArchivesPage 组件,三级结构:年份 → 月份 → 文章 - Header/NavItem/Footer 改为 pub 共享,NavItem 通过路由动态判断高亮 - 编译 Tailwind CSS 包含归档页面所需 utility classes Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a15394c935
commit
7599bfbb13
@ -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);
|
||||
|
||||
205
src/pages/archives.rs
Normal file
205
src/pages/archives.rs
Normal file
@ -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<MonthGroup>,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
struct MonthGroup {
|
||||
month: &'static str,
|
||||
month_en: &'static str,
|
||||
posts: Vec<Post>,
|
||||
}
|
||||
|
||||
fn group_posts(posts: &[Post]) -> Vec<YearGroup> {
|
||||
let mut years: Vec<YearGroup> = 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::<usize>();
|
||||
|
||||
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}",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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::<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 || {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
pub mod admin;
|
||||
pub mod archives;
|
||||
pub mod home;
|
||||
pub mod login;
|
||||
pub mod register;
|
||||
|
||||
@ -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" }
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user