Skip to main content

yggdrasil/components/
post_card.rs

1//! 文章卡片组件
2//!
3//! 在首页、标签详情等列表中展示单篇文章的标题、摘要、封面、日期与标签。
4
5use dioxus::prelude::*;
6use dioxus::router::components::Link;
7
8use crate::models::post::PostListItem;
9use crate::router::Route;
10
11/// 文章卡片组件。
12///
13/// Props:
14/// - `post`:文章数据模型
15///
16/// 展示内容包括:
17/// - 封面图(如有,使用 400x300 缩略图,不启用灯箱)
18/// - 文章标题
19/// - 摘要(最多两行)
20/// - 发布日期与标签
21///
22/// 交互模型(采用覆盖层链接,避免 `<a>` 嵌套 `<a>` 的非法 HTML):
23/// - 整张卡片可点击跳转到文章详情:通过末尾一个绝对定位、铺满卡片的覆盖层 `Link` 实现。
24/// - 标签是独立的 `Link`,通过 `relative z-10` 叠在覆盖层之上,并 `stop_propagation`,
25///   点击标签进入标签详情页而不触发卡片跳转。
26/// - 封面用裸 `.blur-img`(纯展示,无灯箱),点击走卡片跳转,避免交互歧义。
27#[component]
28pub fn PostCard(post: PostListItem) -> Element {
29    let post_slug = post.slug.clone();
30    let date_str = post.formatted_date();
31    let cover_src = post.cover_image.clone().unwrap_or_default();
32    let has_cover = post.cover_image.is_some();
33
34    rsx! {
35        article { class: "group relative mb-12 flex flex-col bg-[var(--color-paper-entry)] rounded-[2rem] border border-transparent hover:border-[var(--color-paper-border)] hover:shadow-sm overflow-hidden transition-all duration-300",
36            if has_cover {
37                div { class: "w-full overflow-hidden",
38                    div { class: "blur-img post-card-cover-blur !rounded-none",
39                        img {
40                            class: "blur-img-placeholder",
41                            src: "{cover_src}?w=20",
42                            alt: "{post.title}",
43                            loading: "lazy",
44                        }
45                        img {
46                            class: "blur-img-full is-loaded",
47                            src: "{cover_src}?thumb=420x180",
48                            alt: "{post.title}",
49                        }
50                    }
51                }
52            }
53            div { class: "p-8 flex flex-col gap-3",
54                h2 { class: "text-2xl md:text-3xl font-extrabold tracking-tight leading-tight text-[var(--color-paper-primary)] group-hover:text-[var(--color-paper-accent)] transition-colors duration-200",
55                    "{post.title}"
56                }
57                div { class: "mt-1 text-base text-[var(--color-paper-secondary)] leading-relaxed line-clamp-2",
58                    "{post.summary.as_deref().unwrap_or_default()}"
59                }
60                div { class: "mt-4 flex flex-wrap items-center gap-3 text-sm font-medium text-[var(--color-paper-tertiary)]",
61                    span { class: "tracking-wide", "{date_str}" }
62                    if !post.tags.is_empty() {
63                        span { "·" }
64                        for tag in post.tags.clone().into_iter() {
65                            span { key: "{tag}",
66                                Link {
67                                    class: "relative z-10 px-3 py-1 rounded-full border border-[var(--color-paper-border)] hover:bg-[var(--color-paper-accent)] hover:border-[var(--color-paper-accent)] hover:text-white transition-all duration-200",
68                                    to: Route::TagDetail {
69                                        tag: tag.clone(),
70                                    },
71                                    onclick: move |evt: dioxus::events::MouseEvent| evt.stop_propagation(),
72                                    "{tag}"
73                                }
74                            }
75                        }
76                    }
77                }
78            }
79            // 覆盖层链接:铺满卡片,承担整卡跳转。
80            // z-[2] 高于封面完整图 (.blur-img-full z-index:1, input.css:611),
81            // 使鼠标 hover 封面时命中本 <a> 而非 <img>:光标变 pointer 且点击可跳转;
82            // 仍低于标签 (z-10),标签独立点击(stop_propagation)不受影响。
83            Link {
84                class: "absolute inset-0 z-[2]",
85                aria_label: "post link to {post.title}",
86                to: Route::PostDetail {
87                    slug: post_slug,
88                },
89            }
90        }
91    }
92}