From 8ae3299b3e16ea497580a25ea306dc4f3bce2e96 Mon Sep 17 00:00:00 2001 From: xfy Date: Thu, 11 Jun 2026 14:36:05 +0800 Subject: [PATCH] feat(comments): add PendingCommentItem component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renders pending (unapproved) comments with: - opacity-70 for visual distinction - amber '审核中' badge - Client-side content_md rendering (HTML escape + newline→br) - No reply button (server rejects replies to pending parents) - Same depth/indent logic as approved comments --- src/components/comments/mod.rs | 1 + src/components/comments/pending_item.rs | 71 +++++++++++++++++++++++++ src/components/comments/section.rs | 2 +- src/hooks/comment_storage.rs | 5 +- 4 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 src/components/comments/pending_item.rs diff --git a/src/components/comments/mod.rs b/src/components/comments/mod.rs index 0411343..d81e597 100644 --- a/src/components/comments/mod.rs +++ b/src/components/comments/mod.rs @@ -2,4 +2,5 @@ pub mod section; pub mod form; pub mod list; pub mod item; +pub mod pending_item; pub mod actions; diff --git a/src/components/comments/pending_item.rs b/src/components/comments/pending_item.rs new file mode 100644 index 0000000..7deedac --- /dev/null +++ b/src/components/comments/pending_item.rs @@ -0,0 +1,71 @@ +use dioxus::prelude::*; + +use crate::hooks::comment_storage::{PendingComment, render_pending_content}; + +#[component] +pub fn PendingCommentItem(comment: PendingComment, post_id: i32) -> Element { + let _ = post_id; + + let depth = if comment.parent_id.is_none() && comment.depth > 0 { + 0 + } else { + comment.depth + }; + + let indent = depth.min(6) * 24; + let content_html = render_pending_content(&comment.content_md); + + let author_element = match &comment.author_url { + Some(url) if !url.is_empty() => rsx! { + a { + href: "{url}", + rel: "nofollow noopener", + target: "_blank", + class: "font-medium text-paper-primary hover:text-paper-accent transition-colors", + "{comment.author_name}" + } + }, + _ => rsx! { + span { class: "font-medium text-paper-primary", + "{comment.author_name}" + } + }, + }; + + rsx! { + div { + class: "py-4 opacity-70", + style: "margin-left: {indent}px", + + div { class: "flex gap-3", + img { + src: "{comment.avatar_url}", + alt: "{comment.author_name} 的头像", + loading: "lazy", + decoding: "async", + class: "w-8 h-8 rounded-full shrink-0 mt-0.5 bg-gray-200 dark:bg-[#2a2a2a]", + } + + div { class: "flex-1 min-w-0", + div { class: "flex items-center gap-1.5 text-sm mb-1.5 flex-wrap", + {author_element} + span { class: "text-paper-tertiary", "·" } + span { + class: "text-paper-tertiary", + "刚刚" + } + span { + class: "inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400", + "审核中" + } + } + + div { + class: "prose prose-sm dark:prose-invert max-w-none text-paper-secondary", + dangerous_inner_html: "{content_html}", + } + } + } + } + } +} diff --git a/src/components/comments/section.rs b/src/components/comments/section.rs index a52e4e0..020a7f9 100644 --- a/src/components/comments/section.rs +++ b/src/components/comments/section.rs @@ -27,7 +27,7 @@ pub fn CommentSection(post_id: i32) -> Element { }); use_future(move || { - let pending = ctx.pending_comments; + let mut pending = ctx.pending_comments; async move { let ids: Vec = pending().iter().map(|c| c.id).collect(); if ids.is_empty() { diff --git a/src/hooks/comment_storage.rs b/src/hooks/comment_storage.rs index 552a01c..448312c 100644 --- a/src/hooks/comment_storage.rs +++ b/src/hooks/comment_storage.rs @@ -13,7 +13,7 @@ pub struct AuthorInfo { pub url: String, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct PendingComment { pub id: i64, pub parent_id: Option, @@ -108,12 +108,13 @@ pub fn load_pending_comments(post_id: i32) -> Vec { let key = post_id.to_string(); let comments = map.remove(&key).unwrap_or_default(); + let original_len = comments.len(); let non_expired: Vec = comments .into_iter() .filter(|c| !is_expired(&c.stored_at)) .collect(); - let pruned = non_expired.len() != comments.len(); + let pruned = non_expired.len() != original_len; if !non_expired.is_empty() { map.insert(key, non_expired.clone()); }