实现博客首页(PaperMod 风格)

- Header: 顶部导航栏,Logo + 首页/归档/标签/搜索/关于 + 主题切换
- FirstEntry: 首篇文章精选区,大标题 + 完整摘要
- PostEntry: 文章卡片列表,圆角边框 + hover 上浮效果
- Pagination: 下一页按钮
- Footer: 版权信息 + 返回顶部按钮
- 6 篇中文占位文章数据

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
xfy 2026-05-26 10:34:54 +08:00
parent 8b3aab21de
commit 452e7d8312

View File

@ -1,8 +1,217 @@
use dioxus::prelude::*;
use crate::theme::ThemeToggle;
#[derive(Clone, PartialEq)]
struct Post {
title: &'static str,
summary: &'static str,
date: &'static str,
tags: &'static [&'static str],
slug: &'static str,
}
const POSTS: &[Post] = &[
Post {
title: "开始使用 Rust 构建 Web 应用",
summary: "Rust 作为一门系统级编程语言,近年来在 Web 开发领域也展现出了强大的生命力。本文将介绍如何使用 Rust 和 Dioxus 框架构建现代化的全栈 Web 应用,从项目搭建到部署的完整流程。",
date: "2026-05-20",
tags: &["Rust", "Web"],
slug: "rust-web-app",
},
Post {
title: "Tailwind CSS 的设计理念与实践",
summary: "Tailwind CSS 是一种实用优先的 CSS 框架,它改变了我们编写样式的方式。通过原子化的工具类,开发者可以快速构建出美观且一致的界面,而无需在 CSS 文件和 HTML 之间来回切换。",
date: "2026-05-15",
tags: &["CSS", "前端"],
slug: "tailwind-css",
},
Post {
title: "PostgreSQL 在 Rust 项目中的最佳实践",
summary: "数据库是大多数 Web 应用的核心组件。本文探讨如何在 Rust 项目中高效地使用 PostgreSQL包括连接池管理、异步查询、事务处理以及常见的性能优化技巧。",
date: "2026-05-10",
tags: &["数据库", "Rust"],
slug: "postgresql-rust",
},
Post {
title: "暗色模式的设计思考",
summary: "暗色模式不仅仅是颜色的反转,它涉及到一整套设计系统的重新思考。从对比度到语义化颜色,暗色模式需要细致的打磨才能提供舒适的阅读体验。",
date: "2026-05-05",
tags: &["设计", "UI"],
slug: "dark-mode-design",
},
Post {
title: "博客系统的架构演进",
summary: "从一个简单的静态页面到全栈应用,博客系统的架构经历了多次演进。本文记录了 Yggdrasil 博客从设计到实现的思考过程,以及每次迭代背后的决策依据。",
date: "2026-04-28",
tags: &["架构", "博客"],
slug: "blog-architecture",
},
Post {
title: "Dioxus 0.7 新特性一览",
summary: "Dioxus 0.7 带来了许多令人兴奋的改进,包括更好的全栈支持、改进的路由系统和更流畅的开发体验。让我们一起看看这些新特性如何提升开发效率。",
date: "2026-04-20",
tags: &["Rust", "框架"],
slug: "dioxus-07",
},
];
#[component]
pub fn HomePage() -> Element {
rsx! {
"Home"
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",
FirstEntry { post: POSTS[0].clone() }
for post in POSTS.iter().skip(1) {
PostEntry { post: post.clone() }
}
Pagination {}
}
Footer {}
}
}
}
#[component]
fn Header() -> Element {
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",
a {
class: "text-2xl font-bold text-gray-900 dark:text-[#dadadb] hover:opacity-80 transition-opacity",
href: "/",
"Yggdrasil"
}
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: "关于" }
}
ThemeToggle {}
}
}
}
}
}
#[component]
fn NavItem(href: &'static str, label: &'static str, #[props(default = false)] active: bool) -> Element {
let base_class = "px-3 py-1 text-base rounded-lg transition-colors";
let class_str = if 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)
};
rsx! {
li {
a { class: "{class_str}", href: "{href}", "{label}" }
}
}
}
#[component]
fn FirstEntry(post: Post) -> Element {
let tag_items = post.tags.iter().map(|t| *t).collect::<Vec<_>>();
rsx! {
article { class: "mb-10",
a { class: "block group", href: "/post/{post.slug}",
h1 { class: "text-[34px] font-bold leading-tight text-gray-900 dark:text-[#dadadb] group-hover:opacity-80 transition-opacity",
"{post.title}"
}
div { class: "mt-3 text-base text-gray-500 dark:text-[#9b9c9d] leading-relaxed",
"{post.summary}"
}
div { class: "mt-4 flex items-center gap-3 text-sm text-gray-400 dark:text-[#9b9c9d]",
span { "{post.date}" }
span { "·" }
for (i, tag) in tag_items.iter().enumerate() {
if i > 0 {
span { "," }
}
span { "{tag}" }
}
}
}
}
}
}
#[component]
fn PostEntry(post: Post) -> Element {
let tag_items = post.tags.iter().map(|t| *t).collect::<Vec<_>>();
rsx! {
article { class: "relative mb-6 p-6 bg-white dark:bg-[#2e2e33] rounded-lg border border-gray-200 dark:border-[#333] hover:-translate-y-0.5 hover:border-gray-300 dark:hover:border-gray-600 transition-all duration-250",
a { class: "block group", href: "/post/{post.slug}",
h2 { class: "text-2xl font-bold leading-tight text-gray-900 dark:text-[#dadadb] group-hover:opacity-80 transition-opacity",
"{post.title}"
}
div { class: "mt-2 text-sm text-gray-500 dark:text-[#9b9c9d] leading-relaxed line-clamp-2",
"{post.summary}"
}
div { class: "mt-3 flex items-center gap-3 text-[13px] text-gray-400 dark:text-[#9b9c9d]",
span { "{post.date}" }
span { "·" }
for (i, tag) in tag_items.iter().enumerate() {
if i > 0 {
span { "," }
}
span { "{tag}" }
}
}
}
}
}
}
#[component]
fn Pagination() -> Element {
rsx! {
nav { class: "flex mt-10 mb-6",
a {
class: "ml-auto inline-flex items-center px-4 py-2 text-sm text-white bg-gray-900 dark:bg-[#dadadb] dark:text-gray-900 rounded-full hover:opacity-80 transition-opacity",
href: "/page/2",
"下一页"
span { class: "ml-1", "»" }
}
}
}
}
#[component]
fn Footer() -> Element {
rsx! {
footer { class: "w-full border-t border-gray-200 dark:border-[#333] mt-auto",
div { class: "max-w-3xl mx-auto px-6 py-5 flex items-center justify-between text-sm text-gray-400 dark:text-[#9b9c9d]",
span { "© 2026 Yggdrasil Blog" }
button {
class: "p-2 rounded-full bg-gray-100 dark:bg-[#333] hover:bg-gray-200 dark:hover:bg-[#444] transition-colors",
onclick: move |_| scroll_to_top(),
""
}
}
}
}
}
fn scroll_to_top() {
#[cfg(target_arch = "wasm32")]
{
if let Some(window) = web_sys::window() {
let mut options = web_sys::ScrollToOptions::new();
options.top(0.0);
options.behavior(web_sys::ScrollBehavior::Smooth);
let _ = window.scroll_to_with_scroll_to_options(&options);
}
}
#[cfg(not(target_arch = "wasm32"))]
{
let _ = ();
}
}