diff --git a/Dioxus.toml b/Dioxus.toml index b876387..e4e6af4 100644 --- a/Dioxus.toml +++ b/Dioxus.toml @@ -10,9 +10,9 @@ title = "Yggdrasil - Dioxus SSR" watch_path = ["src", "Cargo.toml"] [web.resource] -style = ["style.css"] +style = ["/style.css"] script = [] [web.resource.dev] -style = ["style.css"] +style = ["/style.css"] script = [] diff --git a/public/style.css b/public/style.css index 29c97cc..61a5d24 100644 --- a/public/style.css +++ b/public/style.css @@ -262,6 +262,9 @@ .mt-4 { margin-top: calc(var(--spacing) * 4); } + .mt-6 { + margin-top: calc(var(--spacing) * 6); + } .mt-10 { margin-top: calc(var(--spacing) * 10); } @@ -351,6 +354,9 @@ .flex-col { flex-direction: column; } + .flex-wrap { + flex-wrap: wrap; + } .items-center { align-items: center; } @@ -415,6 +421,9 @@ .bg-gray-50 { background-color: var(--color-gray-50); } + .bg-gray-100 { + background-color: var(--color-gray-100); + } .bg-gray-900 { background-color: var(--color-gray-900); } @@ -640,6 +649,13 @@ } } } + .hover\:bg-gray-200 { + &:hover { + @media (hover: hover) { + background-color: var(--color-gray-200); + } + } + } .hover\:bg-red-700 { &:hover { @media (hover: hover) { @@ -647,6 +663,13 @@ } } } + .hover\:text-gray-600 { + &:hover { + @media (hover: hover) { + color: var(--color-gray-600); + } + } + } .hover\:text-gray-700 { &:hover { @media (hover: hover) { @@ -831,6 +854,15 @@ } } } + .dark\:hover\:bg-\[\#333\] { + &:where(.dark, .dark *) { + &:hover { + @media (hover: hover) { + background-color: #333; + } + } + } + } .dark\:hover\:text-\[\#dadadb\] { &:where(.dark, .dark *) { &:hover { diff --git a/src/pages/home.rs b/src/pages/home.rs index e47b868..eedbe5c 100644 --- a/src/pages/home.rs +++ b/src/pages/home.rs @@ -4,15 +4,15 @@ use crate::router::Route; 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, +pub struct Post { + pub title: &'static str, + pub summary: &'static str, + pub date: &'static str, + pub tags: &'static [&'static str], + pub slug: &'static str, } -const POSTS: &[Post] = &[ +pub const POSTS: &[Post] = &[ Post { title: "开始使用 Rust 构建 Web 应用", summary: "Rust 作为一门系统级编程语言,近年来在 Web 开发领域也展现出了强大的生命力。本文将介绍如何使用 Rust 和 Dioxus 框架构建现代化的全栈 Web 应用,从项目搭建到部署的完整流程。", @@ -107,6 +107,7 @@ pub fn NavItem(href: &'static str, label: &'static str, route: Route) -> Element ("/", Route::HomePage {}) => true, ("/archives", Route::ArchivesPage {}) => true, ("/tags", Route::TagsPage {}) => true, + ("/tags", Route::TagDetailPage { .. }) => true, ("/search", Route::SearchPage {}) => true, ("/about", Route::AboutPage {}) => true, _ => false, diff --git a/src/pages/mod.rs b/src/pages/mod.rs index 27d531e..cc8c1a8 100644 --- a/src/pages/mod.rs +++ b/src/pages/mod.rs @@ -3,3 +3,4 @@ pub mod archives; pub mod home; pub mod login; pub mod register; +pub mod tags; diff --git a/src/pages/tags.rs b/src/pages/tags.rs new file mode 100644 index 0000000..e06296e --- /dev/null +++ b/src/pages/tags.rs @@ -0,0 +1,136 @@ +use dioxus::prelude::*; + +use crate::pages::home::{Footer, Header, Post, POSTS}; + +#[derive(Clone, PartialEq)] +struct TagInfo { + name: &'static str, + count: usize, +} + +fn collect_tags() -> Vec { + use std::collections::HashMap; + + let mut counts: HashMap<&'static str, usize> = HashMap::new(); + for post in POSTS.iter() { + for tag in post.tags.iter() { + *counts.entry(*tag).or_insert(0) += 1; + } + } + + let mut tags: Vec = counts + .into_iter() + .map(|(name, count)| TagInfo { name, count }) + .collect(); + + tags.sort_by(|a, b| a.name.cmp(b.name)); + tags +} + +fn posts_for_tag(tag: &str) -> Vec { + POSTS + .iter() + .filter(|p| p.tags.iter().any(|t| *t == tag)) + .cloned() + .collect() +} + +#[component] +pub fn TagsPage() -> Element { + let tags = collect_tags(); + let total_posts = POSTS.len(); + + 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]", "{tags.len()}" } + " 个标签," + span { class: "font-medium text-gray-700 dark:text-[#dadadb]", "{total_posts}" } + " 篇文章" + } + } + ul { class: "flex flex-wrap gap-4 mt-6", + for tag in tags.iter() { + li { + a { + class: "inline-flex items-center px-3 py-1.5 text-base font-medium bg-gray-100 dark:bg-[#2e2e33] text-gray-700 dark:text-[#9b9c9d] rounded-lg hover:bg-gray-200 dark:hover:bg-[#333] transition-colors", + href: "/tags/{tag.name}", + "{tag.name}" + sup { class: "ml-1 text-sm text-gray-500 dark:text-[#9b9c9d]", "{tag.count}" } + } + } + } + } + } + Footer {} + } + } +} + +#[component] +pub fn TagDetailPage(tag: String) -> Element { + let posts = posts_for_tag(&tag); + + 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]", + "{tag}" + } + 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 post in posts.iter() { + TagPostEntry { post: post.clone() } + } + } + Footer {} + } + } +} + +#[component] +fn TagPostEntry(post: Post) -> Element { + let tag_items = post.tags.iter().map(|t| *t).collect::>(); + + 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, t) in tag_items.iter().enumerate() { + if i > 0 { + span { "," } + } + span { + a { + class: "hover:text-gray-600 dark:hover:text-[#dadadb] transition-colors", + href: "/tags/{t}", + "{t}" + } + } + } + } + } + } + } +} diff --git a/src/router.rs b/src/router.rs index 0388d05..dfea070 100644 --- a/src/router.rs +++ b/src/router.rs @@ -5,6 +5,7 @@ use crate::pages::archives::ArchivesPage; use crate::pages::home::HomePage; use crate::pages::login::LoginPage; use crate::pages::register::RegisterPage; +use crate::pages::tags::{TagsPage, TagDetailPage}; use crate::theme::{Theme, ThemePreload, use_theme_provider}; #[derive(Clone, Routable, Debug, PartialEq)] @@ -21,6 +22,8 @@ pub enum Route { ArchivesPage {}, #[route("/tags")] TagsPage {}, + #[route("/tags/:tag")] + TagDetailPage { tag: String }, #[route("/search")] SearchPage {}, #[route("/about")] @@ -44,11 +47,6 @@ pub fn AppRouter() -> Element { } } -#[component] -pub fn TagsPage() -> Element { - rsx! { "Tags" } -} - #[component] pub fn SearchPage() -> Element { rsx! { "Search" }