Skip to main content

yggdrasil/components/comments/
item.rs

1//! 单条评论项组件
2//!
3//! 展示已审核通过的评论,支持展开/收起回复表单。
4
5use leptos::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` 内的草稿信号(`content_md` 等)会随组件
23///   卸载被销毁,取消回复或切换回复目标时静默丢失草稿
24#[component]
25pub fn CommentItem(comment: PublicComment, post_id: i32) -> impl IntoView {
26    let ctx = expect_context::<CommentContext>();
27    let 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 comment_id = comment.id;
37    let is_replying = Memo::new(move |_| active_reply.get() == Some(comment_id));
38    let show_reply = depth < 20;
39    // 本评论卡片自身的缩进像素值(与 CommentCardShell 的 style 计算一致),
40    // 传给回复表单用于抵消缩进、把表单拉回内容区左边缘。
41    let indent_px = if depth > 1 {
42        (depth.min(5) - 1) * 16
43    } else {
44        0
45    };
46
47    let author_element = match &comment.author_url {
48        Some(url) if !url.is_empty() => view! {
49            <a class="comment-author" href=url.clone() rel="nofollow noopener" target="_blank">
50                {comment.author_name.clone()}
51            </a>
52        }
53        .into_any(),
54        _ => view! { <span class="comment-author">{comment.author_name.clone()}</span> }.into_any(),
55    };
56
57    let author_badge = if comment.is_author {
58        view! { <span class="comment-badge">"作者"</span> }.into_any()
59    } else {
60        ().into_any()
61    };
62
63    let timestamp = view! {
64        <time
65            class="comment-time"
66            datetime=comment.created_at_iso.clone()
67            title=comment.created_at_iso.clone()
68        >
69            {comment.created_at.clone()}
70        </time>
71    }
72    .into_any();
73
74    let reply_label = format!("回复 {} 的评论", comment.author_name);
75
76    view! {
77        <CommentCardShell
78            depth=depth
79            avatar_url=comment.avatar_url.clone()
80            author_name=comment.author_name.clone()
81            author_element=author_element
82            author_badge=author_badge
83            timestamp=timestamp
84            status_badge=().into_any()
85            content_html=comment.content_html.clone().unwrap_or_default()
86            content_extra_class="md-content"
87        >
88            {show_reply
89                .then(|| {
90                    view! {
91                        <div class="comment-actions">
92                            <button
93                                class="comment-reply"
94                                type="button"
95                                aria-expanded=move || is_replying.get().to_string()
96                                aria-label=move || {
97                                    if is_replying.get() {
98                                        "收起回复".to_string()
99                                    } else {
100                                        reply_label.clone()
101                                    }
102                                }
103                                on:click=move |_| {
104                                    if is_replying.get_untracked() {
105                                        active_reply.set(None);
106                                    } else {
107                                        active_reply.set(Some(comment_id));
108                                    }
109                                }
110                            >
111                                {move || if is_replying.get() { "收起回复" } else { "回复" }}
112                            </button>
113                        </div>
114                    }
115                })}
116
117            <div class=move || if is_replying.get() { "comment-reply-form" } else { "hidden" }>
118                <CommentForm
119                    post_id=post_id
120                    parent_id=Some(comment_id)
121                    parent_indent=Some(indent_px)
122                />
123            </div>
124        </CommentCardShell>
125    }
126}