Skip to main content

yggdrasil/components/comments/
item.rs

1//! 单条评论项组件
2//!
3//! 展示已审核通过的评论,支持展开/收起回复表单。
4
5use dioxus::prelude::*;
6
7use crate::components::comments::form::CommentForm;
8use crate::components::comments::section::CommentContext;
9use crate::models::comment::PublicComment;
10
11/// 单条已审核评论组件。
12///
13/// Props:
14/// - `comment`:已审核评论数据
15/// - `post_id`:所属文章 ID
16///
17/// 关键行为:
18/// - 点击"回复"按钮切换该评论下方的回复表单
19/// - 最大递归深度限制为 20,超过后隐藏回复按钮
20#[component]
21pub fn CommentItem(comment: PublicComment, post_id: i32) -> Element {
22    let ctx: CommentContext = use_context();
23    let mut active_reply = ctx.active_reply;
24
25    // 孤儿评论按顶层展示
26    let depth = if comment.parent_id.is_none() && comment.depth > 0 {
27        0
28    } else {
29        comment.depth
30    };
31
32    let indent = depth.min(6) * 24;
33
34    let is_replying = active_reply() == Some(comment.id);
35    let show_reply = depth < 20;
36
37    // 作者名展示为链接或普通文本
38    let author_element = match &comment.author_url {
39        Some(url) if !url.is_empty() => rsx! {
40            a {
41                href: "{url}",
42                rel: "nofollow noopener",
43                target: "_blank",
44                class: "font-medium text-paper-primary hover:text-paper-accent transition-colors",
45                "{comment.author_name}"
46            }
47        },
48        _ => rsx! {
49            span { class: "font-medium text-paper-primary", "{comment.author_name}" }
50        },
51    };
52
53    rsx! {
54        div { class: "py-4", style: "margin-left: {indent}px",
55
56            div { class: "flex gap-3",
57                img {
58                    src: "{comment.avatar_url}",
59                    alt: "{comment.author_name} 的头像",
60                    loading: "lazy",
61                    decoding: "async",
62                    class: "w-8 h-8 rounded-full shrink-0 mt-0.5 bg-gray-200 dark:bg-gray-800",
63                }
64
65                div { class: "flex-1 min-w-0",
66                    div { class: "flex items-center gap-1.5 text-sm mb-1.5 flex-wrap",
67                        {author_element}
68                        span { class: "text-paper-tertiary", "·" }
69                        span {
70                            class: "text-paper-tertiary",
71                            title: "{comment.created_at_iso}",
72                            "{comment.created_at}"
73                        }
74                    }
75
76                    div {
77                        class: "prose prose-sm dark:prose-invert max-w-none text-paper-secondary md-content",
78                        dangerous_inner_html: comment.content_html.as_deref().unwrap_or(""),
79                    }
80
81                    div { class: "flex items-center gap-3 mt-2",
82                        if show_reply {
83                            button {
84                                class: "text-xs text-paper-tertiary hover:text-paper-accent transition-colors cursor-pointer",
85                                aria_label: "回复 {comment.author_name} 的评论",
86                                onclick: move |_| {
87                                    if is_replying {
88                                        active_reply.set(None);
89                                    } else {
90                                        active_reply.set(Some(comment.id));
91                                    }
92                                },
93                                if is_replying {
94                                    "取消回复"
95                                } else {
96                                    "回复"
97                                }
98                            }
99                        }
100
101                    }
102
103                    if is_replying {
104                        CommentForm {
105                            post_id,
106                            parent_id: Some(comment.id),
107                            parent_indent: Some(indent),
108                        }
109                    }
110                }
111            }
112        }
113    }
114}