feat(comments): add PendingCommentItem component

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
This commit is contained in:
xfy 2026-06-11 14:36:05 +08:00
parent 12a91e3b8e
commit 8ae3299b3e
4 changed files with 76 additions and 3 deletions

View File

@ -2,4 +2,5 @@ pub mod section;
pub mod form;
pub mod list;
pub mod item;
pub mod pending_item;
pub mod actions;

View File

@ -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}",
}
}
}
}
}
}

View File

@ -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<i64> = pending().iter().map(|c| c.id).collect();
if ids.is_empty() {

View File

@ -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<i64>,
@ -108,12 +108,13 @@ pub fn load_pending_comments(post_id: i32) -> Vec<PendingComment> {
let key = post_id.to_string();
let comments = map.remove(&key).unwrap_or_default();
let original_len = comments.len();
let non_expired: Vec<PendingComment> = 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());
}