实现 PaperMod 风格标签页面:标签列表 + 标签详情

- 新增 /tags 页面:展示所有标签及文章数量,按字母排序
- 新增 /tags/:tag 动态路由:展示该标签下的文章列表
- Post/POSTS 改为 pub,供 tags 模块复用
- NavItem 高亮也匹配 TagDetailPage
- 修复 CSS 路径为绝对路径,解决子路由下样式失效

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
xfy 2026-05-26 13:43:38 +08:00
parent 7599bfbb13
commit 06302b14de
6 changed files with 182 additions and 14 deletions

View File

@ -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 = []

View File

@ -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 {

View File

@ -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,

View File

@ -3,3 +3,4 @@ pub mod archives;
pub mod home;
pub mod login;
pub mod register;
pub mod tags;

136
src/pages/tags.rs Normal file
View File

@ -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<TagInfo> {
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<TagInfo> = 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<Post> {
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::<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, 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}"
}
}
}
}
}
}
}
}

View File

@ -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" }