实现 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:
parent
7599bfbb13
commit
06302b14de
@ -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 = []
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
136
src/pages/tags.rs
Normal 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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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" }
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user