Skip to main content

yggdrasil/components/comments/
list.rs

1//! 评论列表组件
2//!
3//! 将已审核评论与待审核评论合并成一棵树并按时间排序渲染。
4
5use dioxus::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)]
14enum MergedComment {
15    Approved(PublicComment),
16    Pending(PendingComment),
17}
18
19/// 合并两类评论并构建成树形结构。
20///
21/// 处理逻辑:
22/// - 将已审核与待审核评论统一为 `MergedComment`
23/// - 若某条评论的 parent_id 不存在于当前集合中,则视为顶层评论
24/// - 同一父节点下的子评论按时间排序
25/// - 使用 DFS 前序遍历生成最终展示顺序
26fn merge_and_treeify(
27    approved: Vec<PublicComment>,
28    pending: Vec<PendingComment>,
29) -> Vec<MergedComment> {
30    use std::collections::{HashMap, HashSet};
31
32    let all: Vec<MergedComment> = approved
33        .into_iter()
34        .map(MergedComment::Approved)
35        .chain(pending.into_iter().map(MergedComment::Pending))
36        .collect();
37
38    let all_ids: HashSet<i64> = all
39        .iter()
40        .map(|c| match c {
41            MergedComment::Approved(c) => c.id,
42            MergedComment::Pending(c) => c.id,
43        })
44        .collect();
45
46    // 按 parent_id 分组,处理指向不存在父节点的 parent_id
47    let mut children_map: HashMap<Option<i64>, Vec<MergedComment>> = HashMap::new();
48    for comment in all {
49        let parent_id = match &comment {
50            MergedComment::Approved(c) => c.parent_id,
51            MergedComment::Pending(c) => c.parent_id,
52        };
53        let effective_parent = match parent_id {
54            Some(pid) if !all_ids.contains(&pid) => None,
55            _ => parent_id,
56        };
57        children_map
58            .entry(effective_parent)
59            .or_default()
60            .push(comment);
61    }
62
63    // 每个父节点下的子评论按创建时间排序
64    for children in children_map.values_mut() {
65        children.sort_by(|a, b| {
66            let time_a = match a {
67                MergedComment::Approved(c) => c.created_at_iso.as_str(),
68                MergedComment::Pending(c) => c.created_at.as_str(),
69            };
70            let time_b = match b {
71                MergedComment::Approved(c) => c.created_at_iso.as_str(),
72                MergedComment::Pending(c) => c.created_at.as_str(),
73            };
74            time_a.cmp(time_b)
75        });
76    }
77
78    // 深度优先遍历生成树形顺序
79    fn dfs(
80        parent_id: Option<i64>,
81        children_map: &HashMap<Option<i64>, Vec<MergedComment>>,
82        result: &mut Vec<MergedComment>,
83    ) {
84        if let Some(children) = children_map.get(&parent_id) {
85            for child in children {
86                result.push(child.clone());
87                let child_id = match child {
88                    MergedComment::Approved(c) => Some(c.id),
89                    MergedComment::Pending(c) => Some(c.id),
90                };
91                dfs(child_id, children_map, result);
92            }
93        }
94    }
95
96    let mut result = Vec::new();
97    dfs(None, &children_map, &mut result);
98    result
99}
100
101/// 评论列表组件。
102///
103/// Props:
104/// - `comments`:已审核评论列表
105/// - `pending`:待审核评论列表
106/// - `post_id`:所属文章 ID
107///
108/// 根据两类评论构建合并树,依次渲染为 `CommentItem` 或 `PendingCommentItem`。
109#[component]
110pub fn CommentList(
111    comments: Vec<PublicComment>,
112    pending: Vec<PendingComment>,
113    post_id: i32,
114) -> Element {
115    let merged = merge_and_treeify(comments, pending);
116
117    rsx! {
118        div { class: "space-y-0 divide-y divide-gray-100 dark:divide-gray-700",
119            for item in merged {
120                match item {
121                    MergedComment::Approved(comment) => rsx! {
122                        CommentItem { key: "{comment.id}", comment, post_id }
123                    },
124                    MergedComment::Pending(comment) => rsx! {
125                        PendingCommentItem { key: "{comment.id}", comment, post_id }
126                    },
127                }
128            }
129        }
130    }
131}