Skip to main content

yggdrasil/components/comments/
item.rs

1//! 单条评论项组件
2//!
3//! 展示已审核通过的评论,支持展开/收起回复表单。
4
5use dioxus::prelude::*;
6
7use crate::components::comments::card::CommentCardShell;
8use crate::components::comments::form::CommentForm;
9use crate::components::comments::section::CommentContext;
10use crate::models::comment::PublicComment;
11
12/// 单条已审核评论组件。
13///
14/// Props:
15/// - `comment`:已审核评论数据
16/// - `post_id`:所属文章 ID
17///
18/// 关键行为:
19/// - 点击"回复"按钮切换该评论下方的回复表单
20/// - 最大递归深度限制为 20,超过后隐藏回复按钮
21/// - 回复表单始终挂载(仅用 `hidden` 类切换可见性),不随 `is_replying` 结构性
22///   卸载——否则 `CommentForm` 内的草稿 `use_signal`(`content_md` 等)会随组件
23///   卸载被销毁,取消回复或切换回复目标时静默丢失草稿
24#[component]
25pub fn CommentItem(comment: PublicComment, post_id: i32) -> Element {
26    let ctx: CommentContext = use_context();
27    let mut active_reply = ctx.active_reply;
28
29    // 孤儿评论按顶层展示
30    let depth = if comment.parent_id.is_none() && comment.depth > 0 {
31        0
32    } else {
33        comment.depth
34    };
35
36    let is_replying = active_reply() == Some(comment.id);
37    let show_reply = depth < 20;
38    // 本评论卡片自身的缩进像素值(与 CommentCardShell 的 style 计算一致),
39    // 传给回复表单用于抵消缩进、把表单拉回内容区左边缘。
40    let indent_px = if depth > 1 {
41        (depth.min(5) - 1) * 16
42    } else {
43        0
44    };
45
46    // 作者名展示为链接或普通文本
47    let author_element = match &comment.author_url {
48        Some(url) if !url.is_empty() => rsx! {
49            a {
50                href: "{url}",
51                rel: "nofollow noopener",
52                target: "_blank",
53                class: "font-semibold text-paper-primary hover:text-paper-accent transition-colors",
54                "{comment.author_name}"
55            }
56        },
57        _ => rsx! {
58            span { class: "font-semibold text-paper-primary", "{comment.author_name}" }
59        },
60    };
61
62    let author_badge = if comment.is_author {
63        rsx! {
64            span { class: "inline-flex items-center px-1.5 py-0.5 rounded-full text-[10px] font-medium bg-[var(--color-paper-accent)]/15 text-[var(--color-paper-accent)]",
65                "作者"
66            }
67        }
68    } else {
69        rsx! {}
70    };
71
72    let timestamp = rsx! {
73        span {
74            class: "text-paper-tertiary",
75            title: "{comment.created_at_iso}",
76            "{comment.created_at}"
77        }
78    };
79
80    rsx! {
81        CommentCardShell {
82            depth,
83            avatar_url: comment.avatar_url.clone(),
84            author_name: comment.author_name.clone(),
85            author_element,
86            author_badge,
87            timestamp,
88            status_badge: rsx! {},
89            content_html: comment.content_html.clone().unwrap_or_default(),
90            content_extra_class: "md-content",
91
92            div { class: "flex items-center gap-3 mt-2",
93                if show_reply {
94                    button {
95                        class: "inline-flex items-center gap-1 text-xs font-medium text-paper-tertiary hover:text-paper-accent hover:bg-[var(--color-paper-entry)] px-2 py-1 rounded-md transition-all cursor-pointer",
96                        class: if is_replying { "text-[var(--color-paper-accent)] bg-[var(--color-paper-accent)]/10" } else { "" },
97                        aria_label: "回复 {comment.author_name} 的评论",
98                        onclick: move |_| {
99                            if is_replying {
100                                active_reply.set(None);
101                            } else {
102                                active_reply.set(Some(comment.id));
103                            }
104                        },
105                        svg {
106                            class: "w-3.5 h-3.5",
107                            fill: "none",
108                            stroke: "currentColor",
109                            stroke_width: "2",
110                            view_box: "0 0 24 24",
111                            path {
112                                stroke_linecap: "round",
113                                stroke_linejoin: "round",
114                                d: "M3 10h10a5 5 0 015 5v2m-15-7l4-4m-4 4l4 4",
115                            }
116                        }
117                        if is_replying {
118                            "取消回复"
119                        } else {
120                            "回复"
121                         }
122                    }
123                }
124            }
125
126            div {
127                class: if is_replying { "mt-2 pt-1" } else { "hidden" },
128                CommentForm {
129                    post_id,
130                    parent_id: Some(comment.id),
131                    parent_indent: Some(indent_px),
132                }
133            }
134        }
135    }
136}