Skip to main content

yggdrasil/components/comments/
list.rs

1//! 评论列表组件
2//!
3//! 将已审核评论与待审核评论合并成一棵树并按时间排序渲染。
4
5use leptos::prelude::*;
6
7use crate::components::comments::item::CommentItem;
8use crate::components::comments::pending_item::PendingCommentItem;
9use crate::models::comment::PublicComment;
10use crate::utils::comment_storage::PendingComment;
11
12/// 合并后的评论节点,可能是已审核或待审核评论。
13#[derive(Clone, PartialEq)]
14enum MergedComment {
15    Approved(PublicComment),
16    Pending(PendingComment),
17}
18
19impl MergedComment {
20    /// 列表行 key:(是否待审核, 评论 id)。待审核占位转为正式评论时 id 不变,
21    /// 带上类别才能让该行重建为 `CommentItem`。
22    fn key(&self) -> (bool, i64) {
23        match self {
24            MergedComment::Approved(c) => (false, c.id),
25            MergedComment::Pending(c) => (true, c.id),
26        }
27    }
28}
29
30/// 合并两类评论并构建成树形结构。
31///
32/// 处理逻辑:
33/// - 将已审核与待审核评论统一为 `MergedComment`
34/// - 若某条评论的 parent_id 不存在于当前集合中,则视为顶层评论
35/// - 同一父节点下的子评论按时间排序
36/// - 使用 DFS 前序遍历生成最终展示顺序
37fn merge_and_treeify(
38    approved: Vec<PublicComment>,
39    pending: Vec<PendingComment>,
40) -> Vec<MergedComment> {
41    use std::collections::{HashMap, HashSet};
42
43    let all: Vec<MergedComment> = approved
44        .into_iter()
45        .map(MergedComment::Approved)
46        .chain(pending.into_iter().map(MergedComment::Pending))
47        .collect();
48
49    let all_ids: HashSet<i64> = all
50        .iter()
51        .map(|c| match c {
52            MergedComment::Approved(c) => c.id,
53            MergedComment::Pending(c) => c.id,
54        })
55        .collect();
56
57    // 按 parent_id 分组,处理指向不存在父节点的 parent_id
58    let mut children_map: HashMap<Option<i64>, Vec<MergedComment>> = HashMap::new();
59    for comment in all {
60        let parent_id = match &comment {
61            MergedComment::Approved(c) => c.parent_id,
62            MergedComment::Pending(c) => c.parent_id,
63        };
64        let effective_parent = match parent_id {
65            Some(pid) if !all_ids.contains(&pid) => None,
66            _ => parent_id,
67        };
68        children_map
69            .entry(effective_parent)
70            .or_default()
71            .push(comment);
72    }
73
74    // 每个父节点下的子评论按创建时间排序
75    for children in children_map.values_mut() {
76        children.sort_by(|a, b| {
77            let time_a = match a {
78                MergedComment::Approved(c) => c.created_at_iso.as_str(),
79                MergedComment::Pending(c) => c.created_at.as_str(),
80            };
81            let time_b = match b {
82                MergedComment::Approved(c) => c.created_at_iso.as_str(),
83                MergedComment::Pending(c) => c.created_at.as_str(),
84            };
85            time_a.cmp(time_b)
86        });
87    }
88
89    // 深度优先遍历生成树形顺序
90    fn dfs(
91        parent_id: Option<i64>,
92        children_map: &HashMap<Option<i64>, Vec<MergedComment>>,
93        result: &mut Vec<MergedComment>,
94    ) {
95        if let Some(children) = children_map.get(&parent_id) {
96            for child in children {
97                result.push(child.clone());
98                let child_id = match child {
99                    MergedComment::Approved(c) => Some(c.id),
100                    MergedComment::Pending(c) => Some(c.id),
101                };
102                dfs(child_id, children_map, result);
103            }
104        }
105    }
106
107    let mut result = Vec::new();
108    dfs(None, &children_map, &mut result);
109    result
110}
111
112/// 评论列表组件。
113///
114/// Props:
115/// - `comments`:已审核评论列表
116/// - `pending`:待审核评论列表
117/// - `post_id`:所属文章 ID
118///
119/// 根据两类评论构建合并树,依次渲染为 `CommentItem` 或 `PendingCommentItem`;
120/// 按 key 复用行,刷新列表时已展开的回复表单与草稿不丢失。
121#[component]
122pub fn CommentList(
123    #[prop(into)] comments: Signal<Vec<PublicComment>>,
124    #[prop(into)] pending: Signal<Vec<PendingComment>>,
125    post_id: i32,
126) -> impl IntoView {
127    let merged = Memo::new(move |_| merge_and_treeify(comments.get(), pending.get()));
128
129    view! {
130        // 评论区灯箱(__initLightbox)的根选择器是 .comment-list .comment-body,绑定在 section.rs。
131        <div class="comment-list">
132            <For
133                each=move || merged.get()
134                key=|item: &MergedComment| item.key()
135                children=move |item: MergedComment| match item {
136                    MergedComment::Approved(comment) => {
137                        view! { <CommentItem comment=comment post_id=post_id/> }.into_any()
138                    }
139                    MergedComment::Pending(comment) => {
140                        view! { <PendingCommentItem comment=comment post_id=post_id/> }.into_any()
141                    }
142                }
143            />
144        </div>
145    }
146}